ConcurrentHashMap是HashMap的線程安全版本,使用了分段加鎖的方案,在高併發時有比較好的性能。html
本文分析JDK1.7中ConcurrentHashMap的實現。java
HashMap不是線程安全的,要實現線程安全除非加鎖,但這樣性能很低。ConcurrentHashMap把整個HashMap數組分紅了若干個Segment,每一個Segment裏有一個數組。添加一個Key時,須要先根據hash值計算出其所在Segment,而後再根據hash值計算出在該Segment中的位置。Segment繼承自ReentrantLock,每一個Segment就是一個鎖。在多線程的狀況下,就減小了鎖競爭,提高了性能。node
ConcurrentHashMap存儲結構以下圖所示:算法
下面咱們來分析源碼,看數據是怎麼存儲的。數組
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; //concurrencyLevel爲併發級別,這一步就是計算出大於concurrencyLevel的最小的2的N次方 //爲何不用HashMap中的Integer.highestOneBit((number - 1) << 1)來計算這個值 //個人理解是concurrencyLevel通常都比較小(默認爲16),採用這種計算方法效率更高。 while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } //後面根據hash計算segment位置時須要用到 this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //計算每個segment中table的length int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
和HashMap最大的不一樣就是多了Segment的初始化。安全
Segment的Size也初始化爲2的N次方,這爲後面的Map總體resize以及肯定一個hash值所在Segment都提供簡便方法。多線程
每一個Segment中的table同HashMap中table同樣,接着來看PUT時怎麼計算Segment的位置。併發
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); //取得Key的Segment位置 int j = (hash >>> segmentShift) & segmentMask; 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); }
segmentShift:在構造函數中計算出來的,假設concurrencyLevel爲16,segmentShift=28(32-4)ssh
segmentMask:15(16-1)函數
可見求Key所在Segment的算法和HashMap中求Key所在table中的位置同樣,都是 hash & (length-1)。
因此這裏Segment的length也必須是2的N次方。
hash >>> segmentShift是爲了使用hash的高位進行與運算。
s.put方法,就是把Key放到Segment中table的響應位置,它的算法和HashMap中相似,只是加入了鎖。
Put一個Key時有下面這段代碼:
void createEntry(int hash, K key, V value, int bucketIndex) { //1.取得鏈表 Entry<K,V> e = table[bucketIndex]; //2.將新Key設置爲鏈表的第一個 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
假設有兩個線程A、B,同時進行第1步,它們獲取到的e是同一個,如:x,y,z
而後線程A運行到第2步,爲e添加了一個新元素a,並賦值給table[bucketIndex],此時table[bucketIndex]爲:a,x,y,z
然後線程B運行到第2步,爲e添加了一個新元素b,並賦值給table[bucketIndex],此時table[bucketIndex]爲:b,x,y,z
因此這種狀況下就會有問題,這只是其中的一個例子,因此HashMap是非線程安全的。
Put一個Key到Table時,使用以下代碼:
final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { ...... } finally { unlock(); } return oldValue; }
能夠看到put時,加入了Lock,這就保證了線程的安全性。
查看ConcurrentHashMap源代碼能夠發現,ConcurrentHashMap的remove、replace等有可能引發線程安全問題的地方都加了Lock。
ConcurrentHashMap的Get方法並非徹底線程安全,由於Get時沒有加鎖,但JDK用了不少volatile類型變量來保證在大多數狀況下的線程安全。
具體怎麼線程不安全,參考:深刻剖析ConcurrentHashMap
ConcurrentHashMap在絕大多數狀況下是線程安全的,在多線程狀況下請使用ConcurrentHashMap。
參考:
1. Java 8系列之從新認識HashMap http://tech.meituan.com/java-hashmap.html
2. 深刻剖析ConcurrentHashMap http://ifeve.com/java-concurrent-hashmap-2
3. jdk8之ConcurrentHashMap源碼解析 http://jahu.iteye.com/blog/2331191