基于Java并發(fā)容器ConcurrentHashMap#put方法解析
jdk1.7.0_79
HashMap可以說是每個(gè)Java程序員用的最多的數(shù)據(jù)結(jié)構(gòu)之一了,無處不見它的身影。關(guān)于HashMap,通常也能說出它不是線程安全的。這篇文章要提到的是在多線程并發(fā)環(huán)境下的HashMap——ConcurrentHashMap,顯然它必然是線程安全的,同樣我們不可避免的要討論散列表,以及它是如何實(shí)現(xiàn)線程安全的,它的效率又是怎樣的,因?yàn)閷?duì)于映射容器還有一個(gè)Hashtable也是線程安全的但它似乎只出現(xiàn)在筆試、面試題里,在現(xiàn)實(shí)編碼中它已經(jīng)基本被遺棄。
關(guān)于HashMap的線程不安全,在多線程并發(fā)環(huán)境下它所帶來的影響絕不僅僅是出現(xiàn)臟數(shù)據(jù)等數(shù)據(jù)不一致的情況,嚴(yán)重的是它有可能帶來程序死循環(huán),這可能有點(diǎn)不可思議,但確實(shí)在不久前的項(xiàng)目里同事有遇到了CPU100%滿負(fù)荷運(yùn)行,分析結(jié)果是在多線程環(huán)境下HashMap導(dǎo)致程序死循環(huán)。對(duì)于Hashtable,查看其源碼可知,Hashtable保證線程安全的方式就是利用synchronized關(guān)鍵字,這樣會(huì)導(dǎo)致效率低下,但對(duì)于ConcurrentHashMap則采用了不同的線程安全保證方式——分段鎖。它不像Hashtable那樣將整個(gè)table鎖住而是將數(shù)組元素分段加鎖,如果線程1訪問的元素在分段segment1,而線程2訪問的元素在分段segment2,則它們互不影響可以同時(shí)進(jìn)行操作。如果合理的進(jìn)行分段就是其關(guān)鍵問題。
ConcurrentHashMap和HashMap的結(jié)果基本一致,同樣也是Entry作為存放數(shù)據(jù)的對(duì)象,另外一個(gè)就是上面提到的分段鎖——Segment。它繼承自ReentrantLock(關(guān)于ReentrantLock,可參考《5.Lock接口及其實(shí)現(xiàn)ReentrantLock》),故它具有ReentrantLock一切特性——可重入,獨(dú)占等。
ConcurrentHashMap的結(jié)構(gòu)圖如下所示:

可以看到相比較于HashMap,ConcurrentHashMap在Entry數(shù)組之上是Segment,這個(gè)就是我們上面提到的分段鎖,合理的確定分段數(shù)就能更好的提高并發(fā)效率,我們來看ConcurrentHashMap是如何確定分段數(shù)的。
ConcurrentHashMap的初始化時(shí)通過其構(gòu)造函數(shù)public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)完成的,若在不指定各參數(shù)的情況下,初始容量initialCapacity=DAFAULT_INITIAL_CAPACITY=16,負(fù)載因子loadFactor=DEFAULT_LOAD_FACTOR=0.75f,并發(fā)等級(jí)concurrencyLevel=DEFAULT_CONCURRENCY_LEVEL=16,前兩者和HashMap相同。至于負(fù)載因子表示一個(gè)散列表的空間的使用程度,initialCapacity(總?cè)萘? * loadFactor(負(fù)載因子) = 數(shù)據(jù)量,有此公式可知,若負(fù)載因子越大,則散列表的裝填程度越高,也就是能容納更多的元素,但這樣元素就多,鏈表就大,此時(shí)索引效率就會(huì)降低。若負(fù)載因子越小,則相反,索引效率就會(huì)高,換來的代價(jià)就是浪費(fèi)的空間越多。并發(fā)等級(jí)它表示估計(jì)最多有多少個(gè)線程來共同修改這個(gè)Map,稍后可以看到它和segment數(shù)組相關(guān),segment數(shù)組的長度就是通過concurrencyLevel計(jì)算得出。
//以默認(rèn)參數(shù)為例initalCapacity=16,loadFactor=0.75,concurrencyLevel=16
public ConcurrentHashMap(int initalCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;//segment數(shù)組長度
while (ssize < concurrencyLevel) {
++sshift;
ssize <= 1;
}//經(jīng)過ssize左移4位后,ssize=16,ssift=4
/*segmentShift用于參與散列運(yùn)算的位數(shù),segmentMask是散列運(yùn)算的掩碼,這里有關(guān)的散列函數(shù)運(yùn)算和HashMap有類似之處*/
this.segmentShift = 32 – ssift;//段偏移量segmentShift=28
this.segmentMask = ssize – 1;//段掩碼segmentMask=15(1111)
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;//c = 1
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;//MIN_SEGMENT_TABLE_CAPACITY=2
while (cap < c)//cap = 2, c = 1,false
cap <<= 1;//cap是segment里HashEntry數(shù)組的長度,最小為2
/*創(chuàng)建segments數(shù)組和segment[0]*/
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[]) new HashEntry[cap]);//參數(shù)意為:負(fù)載因子=1,數(shù)據(jù)容量=(int)(2 * 0.75)=1,總?cè)萘?2,故每個(gè)Segment的HashEntry總?cè)萘繛?,實(shí)際數(shù)據(jù)容量為1
Segment<K,V> ss = (Segment<K,V>[])new Segment[ssize];//segments數(shù)組大小為16
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
以上就是整個(gè)初始化過程,主要是初始化segments的長度大小以及通過負(fù)載因子確定每個(gè)Segment的容量大小。確定好Segment過后,接下來的重點(diǎn)就是如何準(zhǔn)確定位Segment。定位Segment的方法就是通過散列函數(shù)來定位,先通過hash方法對(duì)元素進(jìn)行二次散列,這個(gè)算法較為復(fù)雜,其目的只有一個(gè)——減少散列沖突,使元素能均勻分布在不同的Segment上,提高容器的存取效率。
我們通過最直觀最常用的put方法來觀察ConcurrentHashMap是如何通過key值計(jì)算hash值在定位到Segment的:
//ConcurrentHashMap#put
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);//根據(jù)散列函數(shù),計(jì)算出key值的散列值
int j = (hash >>> segmentShift) & segmentMask;//這個(gè)操作就是定位Segment的數(shù)組下標(biāo),jdk1.7之前是segmentFor返回Segment,1.7之后直接就取消了這個(gè)方法,直接計(jì)算數(shù)組下標(biāo),然后通過偏移量底層操作獲取Segment
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);//通過便宜量定位不到就調(diào)用ensureSegment方法定位Segment
return s.put(key, hash, value, false);
}
Segment.put方法就是將鍵、值構(gòu)造為Entry節(jié)點(diǎn)加入到對(duì)應(yīng)的Segment段里,如果段中已經(jīng)有元素(即表示兩個(gè)key鍵值的hash值重復(fù))則將最新加入的放到鏈表的頭),整個(gè)過程必然是加鎖安全的。

不妨繼續(xù)深入Segment.put方法:
//Segment#put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);//非阻塞獲取鎖,獲取成功node=null,失敗
V oldValue;
try {
HashEntry<K,V>[] tab = table;//Segment對(duì)應(yīng)的HashEntry數(shù)組長度
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);//獲取HashEntry數(shù)組的第一個(gè)值
for (HashEntry<K,V> e = first;;) {
if (e != null) {//HashEntry數(shù)組已經(jīng)存在值
K k;
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {//key值和hash值都相等,則直接替換舊值
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;//不是同一個(gè)值則繼續(xù)遍歷,直到找到相等的key值或者為null的HashEntry數(shù)組元素
}
else {//HashEntry數(shù)組中的某個(gè)位置元素為null
if (node != null)
node.setNext(first);//將新加入節(jié)點(diǎn)(key)的next引用指向HashEntry數(shù)組第一個(gè)元素
else//已經(jīng)獲取到了Segment鎖
node = new HashEntry<K,V>(hash, key, value, first)
int c = count + 1;
if (c > threshold && tab.lenth < MAXIUM_CAPACITY)//插入前先判斷是否擴(kuò)容,ConcurrentHashMap擴(kuò)容與HashMap不同,ConcurrentHashMap只擴(kuò)Segment的容量,HashMap則是整個(gè)擴(kuò)容
rehash(node);
else
setEntryAt(tab, index, node);//設(shè)置為頭節(jié)點(diǎn)
++modCount;//總?cè)萘?
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
上面大致就是ConcurrentHashMap加入一個(gè)元素的過程,需要明白的就是ConcurrentHashMap分段鎖的概念。在JDK1.6中定位Segment較為簡單,直接計(jì)算出Segment數(shù)組下標(biāo)后就返回具體的Segment,而JDK1.7則通過偏移量來計(jì)算,算出為空時(shí),還有一次檢查獲取Segment,猜測是1.7使用底層native是為了提高效率,JDK1.8的ConcurrentHashMap又有不同,暫未深入研究,它的數(shù)據(jù)結(jié)果似乎變成了紅黑樹。
有關(guān)ConcurrentHashMap的get方法不再分析,過程總結(jié)為一句話:根據(jù)key值計(jì)算出hash值,根據(jù)hash值計(jì)算出對(duì)應(yīng)的Segment,再在Segment下的HashEntry鏈表遍歷查找。
以上這篇基于Java并發(fā)容器ConcurrentHashMap#put方法解析就是小編分享給大家的全部內(nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Intellij IDEA下Spring Boot熱切換配置
這篇文章主要介紹了Intellij IDEA下Spring Boot熱切換配置,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Java8學(xué)習(xí)教程之lambda表達(dá)式語法介紹
眾所周知lambda表達(dá)式是JAVA8中提供的一種新的特性,它支持Java也能進(jìn)行簡單的“函數(shù)式編程”。 下面這篇文章主要給大家介紹了關(guān)于Java8學(xué)習(xí)教程之lambda表達(dá)式語法的相關(guān)資料,需要的朋友可以參考下。2017-09-09
Spring框架開發(fā)IOC兩種創(chuàng)建工廠方法詳解
這篇文章主要介紹了Spring框架IOC兩種創(chuàng)建工廠方法詳解,文中附含詳細(xì)的代碼示例分別對(duì)靜態(tài)方法和實(shí)例方法創(chuàng)建工廠作了簡要的分析2021-09-09
java獲取ip地址與網(wǎng)絡(luò)接口的方法示例
這篇文章主要給大家介紹了關(guān)于利用java如何獲取ip地址與網(wǎng)絡(luò)接口的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01
Java工程中使用Mybatis (工程結(jié)合Mybatis,數(shù)據(jù)結(jié)合Swing使用))
這篇文章主要介紹了Java工程中使用Mybatis (工程結(jié)合Mybatis,數(shù)據(jù)可以結(jié)合Swing使用),需要的朋友可以參考下2017-04-04
詳解MyEclipse中搭建spring-boot+mybatis+freemarker框架
這篇文章主要介紹了詳解MyEclipse中搭建spring-boot+mybatis+freemarker框架,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10

