1. 程式人生 > >Java Collection秋招復習

Java Collection秋招復習

抽象類和介面的區別

我們先來看一下抽象類

 * @auther draymonder
 */
public abstract class AbstractClassTest {
    private int Test1;

    public int Test2;

    public void test1() {
        return ;
    }

    protected void test2() {
        return ;
    }

    private void test3() {
        return ;
    }

    void test4() {
        return ;
    }

    public abstract void test5();

    protected abstract void test6();

    public static void test7() {
        return ;
    }
}

我們再來看一下介面

/**
 * @auther draymonder
 */
public interface IntefaceTest {
    public int Test1 = 0;

    void test1();

    default void test2() {
        return ;
    }

    public static void test3() {
        return ;
    }
}

由此我們可以知道

  1. 介面中沒有構造方式
  2. 介面中的方法必須是抽象的(在JDK8interface可以使用default實現方法)
  3. 介面中除了static、final變數,不能有其他變數
  4. 介面支援多繼承

Java集合

ArrayList

陣列的預設大小為 10。

private static final int DEFAULT_CAPACITY = 10;

新增元素時使用 ensureCapacityInternal() 方法來保證容量足夠,如果不夠時,需要使用 grow() 方法進行擴容,新容量的大小為 oldCapacity + (oldCapacity >> 1),也就是舊容量的 1.5 倍。

Vector

陣列的預設大小為 10。

Vector 每次擴容請求其大小的 2 倍空間,而 ArrayList 是 1.5 倍。

Vector 是同步的,因此開銷就比 ArrayList 要大,訪問速度更慢。最好使用 ArrayList 而不是 Vector,因為同步操作完全可以由程式設計師自己來控制;

可以使用collections的同步list的方法

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

CopyOnWriteArrayList

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

適用場景

CopyOnWriteArrayList 在寫操作的同時允許讀操作,大大提高了讀操作的效能,因此很適合讀多寫少的應用場景。

但是 CopyOnWriteArrayList 有其缺陷:

  • 記憶體佔用:在寫操作時需要複製一個新的陣列,使得記憶體佔用為原來的兩倍左右;
  • 資料不一致:讀操作不能讀取實時性的資料,因為部分寫操作的資料還未同步到讀陣列中。
    所以 CopyOnWriteArrayList 不適合記憶體敏感以及對實時性要求很高的場景。

hashMap

hash為2的冪的作用

  1. key & (hash - 1)等同於key % hash,但前者效率比後者高
  2. 擴容的時候,table cap變為2 * table cap,rehash僅僅需要判斷key & hash如果為0,還是原來的table[old],否則是table[old+table cap]

    mask碼的作用

    先考慮如何求一個數的掩碼,對於 10010000,它的掩碼為 11111111,可以使用以下方法得到:
mask |= mask >> 1    11011000
mask |= mask >> 2    11111110
mask |= mask >> 4    11111111

mask+1 是大於原始數字的最小的 2 的 n 次方。

num     10010000
mask+1 100000000

以下是 HashMap 中計算陣列容量的程式碼:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

JDK7版本下的連結串列迴圈

在擴容時候,由於是頭插法,所以,原來是A->B,但是多執行緒情況下會出現。
執行緒1剛剛拿出A, 並準備rehash到B的後面,但是存在B->A還沒有解除的情況,因此正好出現了A->B->A的情況

hashmap為什麼load factor為0.75

如果load factor太小,那麼空間利用率太低;如果load factor太大,那麼hash衝撞就會比較多

JDK8下hashmap為什麼為長度為8連結串列轉為紅黑樹

我們來看一下hashmap的註釋

Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins.  In
usages with well-distributed user hashCodes, tree bins are
rarely used.  Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:

0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million

我們去有道翻譯translate一下

因為樹節點的大小大約是普通節點的兩倍,所以我們
只有當容器中包含足夠的節點以保證使用時才使用它們
(見TREEIFY_THRESHOLD)。當它們變得太小的時候
移除或調整大小)它們被轉換回普通的箱子。在
使用分佈良好的使用者雜湊碼,樹箱是
很少使用。理想情況下,在隨機雜湊碼下
箱中的節點遵循泊松分佈
(http://en.wikipedia.org/wiki/Poisson_distribution)
預設大小調整的引數平均約為0.5
閾值為0.75,雖然由於方差較大
調整粒度。忽略方差,得到期望
列表大小k的出現次數為(exp(-0.5) * pow(0.5, k) /
階乘(k))。第一個值是:
0:0.60653066
1:0.30326533
2:0.07581633
3:0.01263606
4:0.00157952
5:0.00015795
6:0.00001316
7:0.00000094
8:0.00000006
多於:少於千分之一

所以,節點插入遵循泊松分佈,因此出現一個桶內8個節點是極小概率事件,所以遇到這種情況我們可以用紅黑樹加速get操作

ConcurrentHashMap

不支援 key為null 也不知支援 value為null

JDK7版本下的

//預設的陣列大小16(HashMap裡的那個陣列)
static final int DEFAULT_INITIAL_CAPACITY = 16;

//擴容因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
//ConcurrentHashMap中的陣列
final Segment<K,V>[] segments

//預設併發標準16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

//Segment是ReentrantLock子類,因此擁有鎖的操作
 static final class Segment<K,V> extends ReentrantLock implements Serializable {
  //HashMap的那一套,分別是陣列、鍵值對數量、閾值、負載因子
  transient volatile HashEntry<K,V>[] table;
  transient int count;
  transient int threshold;
  final float loadFactor;

  Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
 }
 
 //換了馬甲還是認識你!!!HashEntry物件,存key、value、hash值以及下一個節點
 static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
 }
//segment中HashEntry[]陣列最小長度
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

//用於定位在segments陣列中的位置,下面介紹
final int segmentMask;
final int segmentShift;

put函式

public V put(K key, V value) {
    Segment<K,V> s;
    //步驟①注意valus不能為空!!!
    if (value == null)
        throw new NullPointerException();
    //根據key計算hash值,key也不能為null,否則hash(key)報空指標
    int hash = hash(key);
    //步驟②派上用場了,根據hash值計算在segments陣列中的位置
    int j = (hash >>> segmentShift) & segmentMask;
    //步驟③檢視當前陣列中指定位置Segment是否為空
    //若為空,先建立初始化Segment再put值,不為空,直接put值。
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

ensureSegement

可以看到JDK7版本下,ConcurrentHashMapsegment也是使用寫時複製的,並且使用CAS演算法來將副本替換

private Segment<K,V> ensureSegment(int k) {
    //獲取segments
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        //拷貝一份和segment 0一樣的segment
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        //大小和segment 0一致,為2
        int cap = proto.table.length;
        //負載因子和segment 0一致,為0.75
        float lf = proto.loadFactor;
        //閾值和segment 0一致,為1
        int threshold = (int)(cap * lf);
        //根據大小建立HashEntry陣列tab
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        //再次檢查
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            根據已有屬性建立指定位置的Segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

put value

首先lock獲取 tab[hash(key)]
然後進行操作

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //步驟① start
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    //步驟① end
    V oldValue;
    try {
        //步驟② start
        //獲取Segment中的HashEntry[]
        HashEntry<K,V>[] tab = table;
        //算出在HashEntry[]中的位置
        int index = (tab.length - 1) & hash;
        //找到HashEntry[]中的指定位置的第一個節點
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            //如果不為空,遍歷這條鏈
            if (e != null) {
                K k;
                //情況① 之前已存過,則替換原值
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                //情況② 另一個執行緒的準備工作
                if (node != null)
                    //連結串列頭插入方式
                    node.setNext(first);
                else //情況③ 該位置為空,則新建一個節點(注意這裡採用連結串列頭插入方式)
                    node = new HashEntry<K,V>(hash, key, value, first);
                //鍵值對數量+1
                int c = count + 1;
                //如果鍵值對數量超過閾值
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    //擴容
                    rehash(node);
                else //未超過閾值,直接放在指定位置
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                //插入成功返回null
                oldValue = null;
                break;
            }
        }
    //步驟② end
    } finally {
        //步驟③
        //解鎖
        unlock();
    }
    //修改成功,返回原值
    return oldValue;
}

scanAndLockForPut

retries64次,不行的話,才用ReentrantLock重入鎖

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    //通過Segment和hash值尋找匹配的HashEntry
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    //重試次數
    int retries = -1; // negative while locating node
    //迴圈嘗試獲取鎖
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //步驟①
        if (retries < 0) {
            //情況① 沒找到,之前表中不存在
            if (e == null) {
                if (node == null) // speculatively create node
                    //新建 HashEntry 備用,retries改成0
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            //情況② 找到,剛好第一個節點就是,retries改成0
            else if (key.equals(e.key))
                retries = 0;
            //情況③ 第一個節點不是,移到下一個,retries還是-1,繼續找
            else
                e = e.next;
        }
        //步驟②
        //嘗試了MAX_SCAN_RETRIES次還沒拿到鎖,簡直B了dog!
        else if (++retries > MAX_SCAN_RETRIES) {
            //泉水掛機
            lock();
            break;
        }
        //步驟③
        //在MAX_SCAN_RETRIES次過程中,key對應的entry發生了變化,則從頭開始
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

最後的put流程

rehash的話 同jdk8版本下的rehash

size

retries2次 如果還是不同,那麼就reentranLock依次等待unlock計算每個tab的size

JDK8下的ConcurrentHashMap

put

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key/value不能為空!!!
    if (key == null || value == null) throw new NullPointerException();
    //計算hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //註釋① 表為null則初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //CAS方法判斷指定位置是否為null,為空則通過建立新節點,通過CAS方法設定在指定位置
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //當前節點正在擴容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //指定位置不為空
        else {
            V oldVal = null;
            //註釋② 加鎖
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //節點是連結串列的情況
                    if (fh >= 0) {
                        binCount = 1;
                        //遍歷整體鏈
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果已存在,替換原值
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            //如果是新加節點,則以尾部插入實現新增
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //節點是紅黑樹的情況
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        //遍歷紅黑樹
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            if (binCount != 0) {
                //連結串列中節點個數超過8轉成紅黑樹
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //註釋③ 新增節點
    addCount(1L, binCount);
    return null;
}

為什麼tab[hash(key)]用cas,但put裡面的元素都需要用synchronized呢

其實hash衝撞的機率蠻低的,所以synchronized呼叫的次數並不多,更多的是在cas那裡...
然後就是cas比synchronized的優點...

size

每次put完畢,都會呼叫addCount方法

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}