1. 程式人生 > 實用技巧 >Java-資料容器-集合-LinkedHashMap

Java-資料容器-集合-LinkedHashMap

1、ConcurrentHashMap

官方文件介紹:

鍵和值是不允許null存在的。
一個支援併發操作的雜湊表,是執行緒安全的。操作方式與Hashtable一致。
獲取資料的操作是非阻塞的,所以在併發讀寫的過程中,讀取到的資料可能已經過時。
迭代器被設計為一次只能由一個執行緒使用.
聚合了狀態的方法(如size,isEmpty,containsValue等)是返回Map瞬時狀態的資訊,可用於做做監控但是不能用作程式控制。
由於Map或進行擴容處理,擴容過程可能相對較慢,所以最好提供一個合理的初始值進行初始化。

2、類層次結構

  • Map-AbstractMap這條線給出了Map操作的骨架實現,
  • Map-ConcurrentMap這條線定義了支援同步的Map基本骨架。

3、建構函式


ConcurrentHashMap提供了5個建構函式,可以根據引數初始化ConcurrentHashMap

3.1、public ConcurrentHashMap()

建立一個使用預設值的空Map。Map容器的初始化延遲到具體操作。

public ConcurrentHashMap() {
}

3.2、public ConcurrentHashMap(int initialCapacity)

建立一個空Map,根據引數設定Map容器的初始化大小,Map容器的初始化延遲到具體操作。

/**儲存資料的容器,建立Map物件是不會初始化容器大小,初始化延遲到第一次資料寫入,容器的大小總是2的冪(與後面計算元素的位置有關),可以通過迭代器直接訪問容器*/
transient volatile Node<K,V>[] table;
/**
控制容器初始化和擴容的狀態,預設值為0,負數和整數表示兩種狀態,
負數時:
      -1表示容器正在初始化或者擴容,
      -n(小於-1)表示n-1個執行緒正在擴容操作,
正數時:
      如果容器為null,則表示容器初始化的長度,
      如果容器不為null,則表示下一次發生擴容的容器長度
*/
private transient volatile int sizeCtl;
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               // 取大於 1.5*initialCapacity+1 的最小2的冪
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

官方文件對sizeCtl的描述不太準確,具體看這篇文章
由於table容器的長度必須是2的冪,所以tableSizeFor會根據引數獲取大於 1.5*initialCapacity+1 的最小2的冪

3.2.1、int tableSizeFor(int c)

/**
根據引數c容量,返回一個大於c的最小2的冪
文件指出了此演算法的出處 《Hackers Delight》,第3.2節
*/
private static final int tableSizeFor(int c) {
    int n = c - 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;
}

這裡使用了5個連續的位移和或運算,得到一個二進位制全為1的數,這個數+1後就得到一個2的冪.
1、假設c=20,二進位制為 0000 0000 0000 0000 0000 0000 0010 0100
2、向右無符號位移一位,得到 0000 0000 0000 0000 0000 0000 0001 0010
3、進行或運算
0000 0000 0000 0000 0000 0000 0010 0100
0000 0000 0000 0000 0000 0000 0001 0010
————————————————————————————————————————————————
0000 0000 0000 0000 0000 0000 0011 0110
4、對 0000 0000 0000 0000 0000 0000 0011 0110 無符號右移兩位 得到 0000 0000 0000 0000 0000 0000 0000 1101
5、或運算
0000 0000 0000 0000 0000 0000 0011 0110
0000 0000 0000 0000 0000 0000 0000 1101
————————————————————————————————————————————————
0000 0000 0000 0000 0000 0000 0011 1111
6、觀察後可以推算出,每次無符號右移n位,就是將最大位為1的位置,向右連續取n位為1
java中int佔4個位元組,1個位元組8個bit,所以int用32bit表示,1+2+4+8+16=31,由於最高位表示符號位,所以5個連續的位移或運算得到一個從最大位開始到低位全為1的值,也就是2的冪-1

3.2.2、與HashMap建構函式對比

引數 初始容器值演算法 含義
HashMap initialCapacity 取大於 initialCapacity 的最小2的冪 initialCapacity 就是實際想要初始化容器長度的值,但是由於容器長度必須是2的冪,所以計算最接近initialCapacity的2的冪,然後設定為擴容閾值,但由於初始化的特殊性,初始化容器時,會將這個2的冪作為容器長度初始化容器。例如如果初始化設定initialCapacity 為15,則計算2的冪得到16,然後第一次put時,table的長度會被設定為16,擴容閾值threshold被重置為16*0.75,所以,再編寫程式碼時,如果我們查詢到一個List,要將資料分組放到Map中以便後面方便取,則可以使用 1.5 * list.size 去初始化Map以減少擴容的發生
ConcurrentHashMap initialCapacity 取大於 1.5*initialCapacity+1 的最小2的冪 initialCapacity更像是想要插入map中的資料長度,然後ConcurrentHashMap根據這個長度計算出一個插入initialCapacity個元素也不會產生擴容的值。例如如果初始化設定initialCapacity為15,則通過計算得到2的冪為32,第一次put時,table的長度會被設定為32,擴容閾值sizeCtl被重置為32*0.75,所以,再編寫程式碼時,如果我們查詢到一個List,要將資料分組放到Map中以便後面方便取,則可以使用 list.size 去初始化Map以減少擴容的發生

3.3、public ConcurrentHashMap(Map<? extends K, ? extends V> m)

/**
根據指定Map建立一個新Map
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 設定table容器預設初始值16
    this.sizeCtl = DEFAULT_CAPACITY;
    // 插入資料
    putAll(m);
}
/**
1、為了防止引數Map的資料較多導致多次擴容,因為CurrentHashMap的擴容耗時較長,所以首先要先根據引數Map的資料個數先嚐試對容器進行擴容,達到減少發生擴容次數的目的
為什麼說是嘗試,因為有可能現有table容器長度已經滿足資料寫入要求,無需擴容
2、迴圈引數Map,插入資料到容器中
*/
public void putAll(Map<? extends K, ? extends V> m) {
    // 以指定Map的size嘗試進行擴容
    tryPresize(m.size());
    // 迴圈插入資料
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}

3.3.1、private final void tryPresize(int size)

/**
根據大小嚐試對容器進行擴容
流程大致如下,由於這裡是建構函式進來,所以先只看初始化容器的程式碼
判斷table容器是否為空
      是
            使用CAS演算法修改sizeCtl 
                  修改成功
                        判斷table容器是否發生變更
                              否
                                    建立初始化table容器,計算下一次擴容閾值,設定sizeCtl 
                                    
*/
private final void tryPresize(int size) {
    // 獲取大於1.5*size+1的最小2的冪
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    /**
      上面提到sizeCtl是有可能小於0的,只有當sizeCtl >= 0 時才能進行擴容,從ConcurrentHashMap(Map<? extends K, ? extends V> m)建構函式進來時,此值為16
      這裡使用了while,說明僅當sc < 0 時才會跳出迴圈,上面提到sizeCtl=-1表示正在擴容或者初始化,所以迴圈內必定會修改sc的值
    */ 
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        if (tab == null || (n = tab.length) == 0) { // 如果table容器為空,則進行容器初始化
            n = (sc > c) ? sc : c; // 比較獲取初始化容器的長度,sizeCtl在建構函式進來時會初始化為預設值16,c是根據引數Map計算出來滿足初始要求的最小容器長度,兩者比較取其大
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 使用CAS演算法更新sizeCtl的值為-1,SIZECTL為sizeCtl的偏移量,在ConcurrentHashMap類載入時就會初始化,sc為期望值,-1位更新值,
                try {
                    if (table == tab) { // 判斷是否有其他執行緒修改了容器,由於併發場景下,上面CAS鎖成功了,存在其他執行緒已經完成容器初始化甚至擴容的動作,所以要做判斷
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 建立容器
                        table = nt; // 初始化容器
                        sc = n - (n >>> 2); // n >>> 2 就是 n/4, n - n/4 = 0.75n,實際就是載入因子*容器長度
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY) // 跳出迴圈,當前容器已經無需擴容或者已達到容器最大邊界
            break;
        else if (tab == table) {// 判斷當前容器是否被其他執行緒初始化
            int rs = resizeStamp(n); // resizeStamp作用不是很明白,看名字與擴容相關, TODO 存在疑問,記錄下,待了解
            if (sc < 0) { // 
                Node<K,V>[] nt;
                // 由於rs看不懂,所以這個if也不清楚是要幹啥,但是break,意味著進入這個if就要跳出迴圈了,那麼這個if推算應該是判斷有其他執行緒正在進行擴容之類的操作,因為我要乾的事已經有人在幫我幹了,所以我直接退出,猜測的,不知道對不對
                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)) // 還是沒看懂,感覺智商不夠用,CAS演算法將sizeCtl的值+1,
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

完成table容器初始化後,就是迴圈引數Map,將資料一條一條插入到table容器中,putVal後面再單獨說
大致過程總結下就是:根據引數map的size計算一個擴容值,比較擴容值和預設值,取其大,然後初始化table容器並設定

3.4、ConcurrentHashMap(int initialCapacity, float loadFactor)

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

呼叫其他建構函式,並設定了個預設值1

3.5、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

根據指定的初始容器長度和載入因子和併發數建立Map容器,這裡要注意與HashMap不同的是ConcurrentHashMap是不允許修改loadFactor的,這裡引數loadFactor只是用來計算初始容器長度值使用

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    // 根據引數初始長度和載入因子推出容器長度
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

不太清楚提供載入因子的建構函式引數的意義在哪裡,既然不提供載入因子的修改,那麼算出來的size對於ConcurrentHashMap來說是不準確的。
例如,initialCapacity=12,loadFactor=0.85,則size=15,cap=16,而ConcurrentHashMap的載入因子是0.75,16*0.75=12,在插入到第12條資料的時候就會發生擴容

4、常用方法

4.1、public V put(K key, V value)

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

put(K key, V value)方法是對外提供插入資料的介面
實際插入的邏輯在putVal(key, value, false)中

4.2、final V putVal(K key, V value, boolean onlyIfAbsent)

key是插入的鍵,value是插入的值,onlyIfAbsent表示是否在key不存在時插入

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); // 與HashMap類似,通過低16位與高16位異或操作,區別在於ConcurrentHashMap多了跟Integer.MAX_VALUE進行與運算操作
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0) // 如果table容器為空,則進行容器初始化
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // (n - 1) & hash計算得到容器下標並賦值給i,f為下標對應的節點資訊
            // CAS演算法在下標i設定節點資訊,如果設定成功,則跳出迴圈,如果不成功,重新進入迴圈執行邏輯
            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) // 判斷容器是否正在擴容,這裡可以看出,擴容時,會修改連結串列頭節點的hash值
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 進入這裡,說明容器已完成初始化,且沒有發生擴容,則需要將節點資訊更新到連結串列或者樹中,synchronized同步鎖鎖住了連結串列頭結點,說明最高支援table.length個執行緒並行寫
            synchronized (f) { 
                if (tabAt(tab, i) == f) { // 判斷下標位的連結串列表頭是否發生改變(例如其他執行緒進行了remove操作)
                    /**
                        檢視後面的else if,可以推斷出,樹節點的hash值一定<0,再觀察if中的程式碼,是對連結串列的遍歷,所以可以猜測,連結串列的節點hash值為正,紅黑樹節點的hash值為負,但是因為
                        後面是else if而不是else,所以這裡是否說明 紅黑樹的節點hash值一定為負數,但是節點hash值為負數不一定就是紅黑樹節點?
                    */
                    if (fh >= 0) { 
                        binCount = 1;
                        // 迴圈遍歷連結串列,binCount為連結串列長度,e為當前連結串列節點資訊
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果key已經存在,則根據onlyIfAbsent判斷是否更新資料,然後跳出迴圈,結束插入操作
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 節點後移,判斷節點是否為空,為空說明迴圈到了連結串列尾部,key不存在,需要在尾部插入新節點
                            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) { // 如果下標位的節點hash值小於0,判斷是否是樹節點
                        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;
                        }
                    }
                }
            }
            // 判斷連結串列是否需要樹形化,如果上面是在樹中插入資料,bitCount為2,不會執行 if(binCount >= TREEIFY_THRESHOLD)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    /*
      新增統計數,方法還蠻複雜,處理統計,還會做擴容時的資料轉移等操作
    */
    addCount(1L, binCount);
    return null;
}
總結:
      1、key、value校驗
      2、獲取key的hash值
      3、死迴圈,直到資料插入成功退出
            判斷容器是否為空
                  是:初始化容器,初始化完成後繼續3,重新執行邏輯
                  否:根據hash值獲取容器下標,判斷容器下標節點是否為空
                        是:使用CAS更新下標位資料
                              更新成功:跳出3迴圈,執行4
                              更新失敗:說明存在併發寫,重新執行3,為了將資料寫入到連結串列末尾
                        否:判斷hash值是否為-1(擴容)
                              是:容器正在擴容,幫助節點資料轉移
                              否:  synchronized同步鎖鎖住下標節點資訊,進行資料寫入
                                    判斷容器下標節點是否發生改變,防止資料被刪除
                                    判斷資料是否寫入成功      
                                          是:根據連結串列長度判斷是否需要連結串列樹化
                                                是:樹化連結串列
                                              跳出3執行4  
                                          否:重新執行3
      4、新增統計數,如果發生擴容,則輔助資料轉移

4.3、public V get(Object key)

根據key獲取value

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 獲取key的hash值
    int h = spread(key.hashCode());
    // 判斷容器不為空且容器下標節點不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0) // 遍歷節點,針對樹節點,返回hash值和key相等的節點資訊,如果沒有匹配到,返回null
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍歷連結串列節點,匹配hash值和key相等的節點資訊
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
總結:
      1、獲取key的hash值
      2、判斷容器不為空且容器下標節點不為空
            是
                  下標節點hash與1的hash值是否相等
                        是
                              判斷下標節點的key是否與引數key相等
                                    是
                                          返回下標節點值
                        否
                              下標節點的hash值是否小於0
                                    是
                                          遍歷節點資訊(樹),返回hash值和key值相等的節點資訊,匹配失敗則返回null
                  遍歷節點資訊(連結串列),返回hash值和key值相等的節點資訊
      3、返回null

4.4、public V remove(Object key)

根據key刪除節點

// 對外刪除入口
public V remove(Object key) {
    return replaceNode(key, null, null);
}
// 實際刪除邏輯程式碼
final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        validated = true;
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

TODO 未完