4.1 java併發容器 - concurrentHashMap

一、HashMap爲什麼是線程不安全的

問題主要出現是hashmap的擴容操作的rehash操作上。

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                //下面兩行代碼我們可以看出在rehash的時候是通過頭插法插入到table中的
                e.next = newTable[i];
                //以下分析都假設在併發時線程A在此刻被掛起
                newTable[i] = e;
                e = next;
            }
        }
    }

1、hashmap會形成死循環,環形鏈表
假設容器初始值爲如下圖、hash的算法簡單的使用取模操作
在這裏插入圖片描述
線程A和B併發向容器中put元素,發現容器使用率已經超過了容器的個人乘以加載因子的值,則需要擴容。

線程A在執行到上述代碼時候時間片結束,此時A的結構爲
在這裏插入圖片描述
此時線程B獲取時間片,進行操作,並且擴容完成。
在這裏插入圖片描述
然後線程A再獲取時間片來進行執行,由java內存模型可知道,newTable和table中的鏈表都是最新的值,A執行完成一輪循環後的結構爲。
在這裏插入圖片描述
繼續第二次循環
在這裏插入圖片描述
此時主存中的7的next是3,此時再將3rehash到table中,此時e已經爲nulll,循環結束,就會出現如下結構
在這裏插入圖片描述之後涉及到輪詢table3的結構時就會發生死循環操作

數據丟失的問題只要將初始化的結構該爲7-》5-》3。最終resize後的結構是如下圖,有興趣的可以自己分析下。出現了環形鏈表和丟失了3
在這裏插入圖片描述

二、concurrentHashMap

JDK1.7的實現

1、concurrentHashMap的數據結構

在這裏插入圖片描述
segment可以看作是一把可重入鎖,因爲它繼承了ReentrantLock,也就是所謂的分段鎖,這裏是在爲併發時候做提高性能使用的。

hashEntry的定義是用volatile關鍵字修飾的,則可以保障他的內存可見性,線程間可以即使看到修改的數據。

static final class HashEntry<K,V> { 
            final K key;                 // 聲明 key 爲 final 型
            final int hash;              // 聲明 hash 值爲 final 型 
            volatile V value;           // 聲明 value 爲 volatile 型
            final HashEntry<K,V> next;  // 聲明 next 爲 final 型
            HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
                this.key = key; 
                this.hash = hash; 
                this.next = next; 
                this.value = value; 
            } 
     }

2、初始化做了哪些事情

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
           implements ConcurrentMap<K, V>, Serializable { 
     
       //默認的segment的大小
       static final     int DEFAULT_INITIAL_CAPACITY= 16; 
    
       //加載因子,當table的佔用個數大於table的容量乘以當前值的時候要觸發擴容,進行rehash
       static final float DEFAULT_LOAD_FACTOR= 0.75f; 
     
       // 散列表的默認併發級別爲 16。該值表示當前更新線程的估計併發量
       static final int DEFAULT_CONCURRENCY_LEVEL= 16; 
     
       /** 
        * segments 的掩碼值
        * key 的散列碼的高位用來選擇具體的 segment 
        * 初始化的時候取的是
        */ 
       final int segmentMask; 
     
       /** 
        * 偏移量
        */ 
       final int segmentShift; 
     
       /** 
        * 由 Segment 對象組成的數組
        */ 
       final Segment<K,V>[] segments; 
     
       /** 
        * 創建一個帶有指定初始容量、加載因子和併發級別的新的空映射。
        */ 
       public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { 
           if(!(loadFactor > 0) || initialCapacity < 0 || 
    concurrencyLevel <= 0) 
               throw new IllegalArgumentException(); 
     
     		//seghment的大小不能超過65535
           if(concurrencyLevel > MAX_SEGMENTS) 
               concurrencyLevel = MAX_SEGMENTS; 
     
           // 尋找最佳匹配參數(不小於給定參數的最接近的 2 次冪) 
           int sshift = 0; 
           int ssize = 1; 
           while(ssize < concurrencyLevel) { 
               ++sshift; 
               ssize <<= 1; 
           } 
           //此時ssize算下來等於16因爲將1左移了4位
           //sshift等於4
           segmentShift = 32 - sshift;       // 偏移量值等於32-4=28這裏是在put操作時候取用的是hash值的前3位來進行的定位segment位置
           segmentMask = ssize - 1;           // 掩碼值,等於15,因爲當前值要
           this.segments = Segment.newArray(ssize);   // 創建數組
     
           if (initialCapacity > MAXIMUM_CAPACITY) 
               initialCapacity = MAXIMUM_CAPACITY; 
           int c = initialCapacity / ssize; 
           if(c * ssize < initialCapacity) 
               ++c; 
           //table的個數是2
           int cap = 1; 
           while(cap < c) 
               cap <<= 1; 
           // 依次遍歷每個數組元素
           for(int i = 0; i < this.segments.length; ++i) 
               // 初始化每個數組元素引用的 Segment 對象
    this.segments[i] = new Segment<K,V>(cap, loadFactor); 
       } 
     
       /** 
        * 創建一個帶有默認初始容量 (16)、默認加載因子 (0.75) 和 默認併發級別 (16) 
     * 的空散列映射表。
        */ 
       public ConcurrentHashMap() { 
           // 使用三個默認參數,調用上面重載的構造函數來創建空散列映射表
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); 
    }

總結下初始化做的事情

  • 計算出segment的掩碼值=15,以爲對segment定位的時候是按照%的方式進行操作的,使用的方式是利用位運算&的方式,因爲a&2^n = a&2的n次方減1,2的4次方等於16.對16取餘就相當於對2的4次方減1=15按位&的操作,這裏一會get\put的時候會用到
  • 計算出cap的值=2,cap的含義就是table中的數組的個數
  • 初始化segment

3、get操作、怎麼定位、如何保證線程安全、get方法的弱一致性

 1    public V get(Object key) {
 2        Segment<K,V> s; // manually integrate access methods to reduce overhead
 3        HashEntry<K,V>[] tab;
 4        int h = hash(key);
 5        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
 6        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
 7            (tab = s.table) != null) {
 8            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
 9                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
10                 e != null; e = e.next) {
11                K k;
12                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
13                    return e.value;
14            }
15        }
16        return null;
17    }

  • 通過獲取到hash的值,如果是自定義對象的話,會要求重寫hashCode方法,hash中是先取到對象的hash值,再進行一個wang jenkis算法的再哈希。獲取到hash值後先先右移segmentShift=28位,取前三位和segmentmask進行按位&運算得到segment的下標
  • 再對segmet中的table進行也進行按位&的操作獲取到table的下標
  • 然後去遍歷鏈表,獲取到對應的值

如何保證線程安全:開始我們看到使用volatile修飾了hashEntry,即保證了線程之間的內存可見性,線程A改變之後不會進行緩存,直接會回寫到內存當中,保證了線程之間的數據可見。

get方法的弱一致性:我們對hashEntry的可見性有了保證但是如果hashEntry已經進行了擴容and rehash則我們查詢的還是舊的鏈表,則會出現get到的數據還是舊數據,這就是get方法的弱一致性。

4、put操作、怎麼定位、如何保證線程安全、key相同是否會覆蓋,那種方法會覆蓋。

 1    public V put(K key, V value) {
 2        Segment<K,V> s;
 3        if (value == null)
 4            throw new NullPointerException();
 5        int hash = hash(key);
 		  //獲取segment的位置
 6        int j = (hash >>> segmentShift) & segmentMask;
 7        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
 8             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
              //如果segmengt還未被初始化,則此處進行一個初始化動作
 9            s = ensureSegment(j);
          //執行put元素的操作
10        return s.put(key, hash, value, false);
11    }
 1        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
              //首先對segmnet進行一個加鎖操作
 2            HashEntry<K,V> node = tryLock() ? null :
 3                scanAndLockForPut(key, hash, value);
 4            V oldValue;
 5            try {
 6                HashEntry<K,V>[] tab = table;
                  //定位segment中的table中的下標
 7                int index = (tab.length - 1) & hash;
 8                HashEntry<K,V> first = entryAt(tab, index);
 9                for (HashEntry<K,V> e = first;;) {
10                    if (e != null) {
11                        K k;
                          //如果hash值相同,key也相同,根據是否需要覆蓋的標識onlyIfAbsent來進行操作,覆蓋的話直接將數據更新,不覆蓋的話直接返回舊值
12                        if ((k = e.key) == key ||
13                            (e.hash == hash && key.equals(k))) {
14                            oldValue = e.value;
15                            if (!onlyIfAbsent) {
16                                e.value = value;
17                                ++modCount;
18                            }
19                            break;
20                        }
21                        e = e.next;
22                    }
23                    else {
24                        if (node != null)
25                            node.setNext(first);
26                        else
27                            node = new HashEntry<K,V>(hash, key, value, first);
28                        int c = count + 1;
29                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
							  //如果當前的table使用已經超過數組大小乘以加載因子則進行擴容和rehash
30                            rehash(node);
31                        else
32                            setEntryAt(tab, index, node);
33                        ++modCount;
34                        count = c;
35                        oldValue = null;
36                        break;
37                    }
38                }
39            } finally {
40                unlock();
41            }
42            return oldValue;
43        }

  • 定位segment位置並加鎖
  • 定位table中的index
  • 去鏈表中查找,根據hashCode和key和onlyIfAbsent來判斷是否需要覆蓋舊值,代碼中有詳細註釋
  • 如果當前的table使用已經超過數組大小乘以加載因子則進行擴容和rehash,並插入要插入的數據
  • 解鎖

JDK1.8實現

1、1.8和1.7之間的變化

  • 取消了segment數據,鎖的粒度直接作用在table上,減少了併發衝突的概率
  • 存儲數據用鏈表+紅黑樹的的形式,紅黑樹的查找速度是log(n),性能很快,。但是插入操作需要進行紅黑樹的平衡調整,所以在8個元素以內使用鏈表的形式,8個元素以上使用紅黑樹的存儲方式

2.主要數據結構和關鍵變量

  • Node和1.7中的hashEntry基本一致,是存儲鏈表時的節點數據
  • sizeCtl
    負數:表示正在初始化或者擴容,-1表示正在初始化、-N表示正在有N個線程進行擴容
    正數:0表示還沒有被初始化,N表示初始化或者下一次擴容的閾值
  • TreeNode 紅黑樹節點
  • TreeBin放在table中數據,也就是紅黑樹的頭節點

3、操作的剖析,都做了哪些事情。

3、初始化

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
//算法的功能爲將你輸入的數字轉換爲距離最近的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;
    }

只是給成員變量賦值,put時進行實際數組的填充

4、get()方法

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //對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;
            }
            //table中存儲的是紅黑樹,需要去紅黑樹中進行查找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //table中存儲的是鏈表,遍歷鏈表進行查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

5、put方法

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

    
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
            	//如果第一次put需要對容器進行初始化
                tab = initTable();
            //如果當前table中沒有元素,則直接將數據插入到當前table的當前位置中
            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
            }
            //當前線程檢測到容易正在擴容,去幫助進行擴容,所做的事情就是將數據進行重新rehash並且搬數據
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //對當前table進行加鎖
                synchronized (f) {
                	//如果當前table存儲的是鏈表。則將數據插入到鏈表中,和1.7操作類似
                    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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                	//如果當前table中存儲的是鏈表,並且數據已經超過了8則進行鏈表到紅黑樹的轉化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

初始化方法

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
        	//如果有其他線程正在初始化,則將當前線程讓出cpu
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //循環操作,使用CAS進行設置sizeCtl爲-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //數組的初始化
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //sc的值設置爲0.75n
                        sc = n - (n >>> 2);
                    }
                } finally {
                	//設置下一次需要擴容的閾值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

這裏需要注意下,在擴容的過程中,如果rehash後的table下的數據小於鏈表於紅黑樹的轉化值(6),則需要將紅黑樹轉化爲鏈表,1.7版本hash的算法是將再散列後值的高位來進行和segmnet的個數減一進行&操作,1.8是用再散列後的值的高16位和tables的大小進行異或操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章