Java ConcurrentHashMap用法案例詳解
一、概念
哈希算法(hash algorithm):是一種將任意內(nèi)容的輸入轉(zhuǎn)換成相同長(zhǎng)度輸出的加密方式,其輸出被稱(chēng)為哈希值。
哈希表(hash table):根據(jù)設(shè)定的哈希函數(shù)H(key)和處理沖突方法將一組關(guān)鍵字映象到一個(gè)有限的地址區(qū)間上,并以關(guān)鍵字在地址區(qū)間中的象作為記錄在表中的存儲(chǔ)位置,這種表稱(chēng)為哈希表或散列,所得存儲(chǔ)位置稱(chēng)為哈希地址或散列地址。
二、HashMap與HashTable
1,線程不安全的HashMap
因?yàn)槎嗑€程環(huán)境下,使用HashMap進(jìn)行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap,如以下代碼
final HashMap<String, String> map = new HashMap<String, String>(2); Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }, "ftf" + i).start(); } } }, "ftf"); t.start(); t.join();
2,效率低下的HashTable容器
HashTable容器使用synchronized來(lái)保證線程安全,但在線程競(jìng)爭(zhēng)激烈的情況下HashTable的效率非常低下。因?yàn)楫?dāng)一個(gè)線程訪問(wèn)HashTable的同步方法時(shí),其他線程訪問(wèn)HashTable的同步方法時(shí),可能會(huì)進(jìn)入阻塞或輪詢(xún)狀態(tài)。如線程1使用put進(jìn)行添加元素,線程2不但不能使用put方法添加元素,并且也不能使用get方法來(lái)獲取元素,所以競(jìng)爭(zhēng)越激烈效率越低。
三、ConcurrentHashMap
1,鎖分段技術(shù)
HashTable容器在競(jìng)爭(zhēng)激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問(wèn)HashTable的線程都必須競(jìng)爭(zhēng)同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而可以有效的提高并發(fā)訪問(wèn)效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn)。
2,ConcurrentHashMap的結(jié)構(gòu)
我們通過(guò)ConcurrentHashMap的類(lèi)圖來(lái)分析ConcurrentHashMap的結(jié)構(gòu)。ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)。一個(gè)ConcurrentHashMap里包含一個(gè)Segment數(shù)組,Segment的結(jié)構(gòu)和HashMap類(lèi)似,是一種數(shù)組和鏈表結(jié)構(gòu), 一個(gè)Segment里包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry是一個(gè)鏈表結(jié)構(gòu)的元素, 每個(gè)Segment守護(hù)者一個(gè)HashEntry數(shù)組里的元素,當(dāng)對(duì)HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須首先獲得它對(duì)應(yīng)的Segment鎖。
3,ConcurrentHashMap的初始化
ConcurrentHashMap初始化方法是通過(guò)initialCapacity,loadFactor, concurrencyLevel幾個(gè)參數(shù)來(lái)初始化segments數(shù)組,段偏移量segmentShift,段掩碼segmentMask和每個(gè)segment里的HashEntry數(shù)組 。
初始化segments數(shù)組。讓我們來(lái)看一下初始化segmentShift,segmentMask和segments數(shù)組的源代碼。
if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } segmentShift = 32 - sshift; segmentMask = ssize - 1; this.segments = Segment.newArray(ssize);
由上面的代碼可知segments數(shù)組的長(zhǎng)度ssize通過(guò)concurrencyLevel計(jì)算得出。為了能通過(guò)按位與的哈希算法來(lái)定位segments數(shù)組的索引,必須保證segments數(shù)組的長(zhǎng)度是2的N次方(power-of-two size),所以必須計(jì)算出一個(gè)是大于或等于concurrencyLevel的最小的2的N次方值來(lái)作為segments數(shù)組的長(zhǎng)度。假如concurrencyLevel等于14,15或16,ssize都會(huì)等于16,即容器里鎖的個(gè)數(shù)也是16。注意concurrencyLevel的最大大小是65535,意味著segments數(shù)組的長(zhǎng)度最大為65536,對(duì)應(yīng)的二進(jìn)制是16位。
初始化segmentShift和segmentMask。這兩個(gè)全局變量在定位segment時(shí)的哈希算法里需要使用,sshift等于ssize從1向左移位的次數(shù),在默認(rèn)情況下concurrencyLevel等于16,1需要向左移位移動(dòng)4次,所以sshift等于4。segmentShift用于定位參與hash運(yùn)算的位數(shù),segmentShift等于32減sshift,所以等于28,這里之所以用32是因?yàn)镃oncurrentHashMap里的hash()方法輸出的最大數(shù)是32位的,后面的測(cè)試中我們可以看到這點(diǎn)。segmentMask是哈希運(yùn)算的掩碼,等于ssize減1,即15,掩碼的二進(jìn)制各個(gè)位的值都是1。因?yàn)閟size的最大長(zhǎng)度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,對(duì)應(yīng)的二進(jìn)制是16位,每個(gè)位都是1。
初始化每個(gè)Segment。輸入?yún)?shù)initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每個(gè)segment的負(fù)載因子,在構(gòu)造方法里需要通過(guò)這兩個(gè)參數(shù)來(lái)初始化數(shù)組中的每個(gè)segment。
if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = 1; while (cap < c) cap <<= 1; for (int i = 0; i < this.segments.length; ++i) this.segments[i] = new Segment<K,V>(cap, loadFactor);
上面代碼中的變量cap就是segment里HashEntry數(shù)組的長(zhǎng)度,它等于initialCapacity除以ssize的倍數(shù)c,如果c大于1,就會(huì)取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。segment的容量threshold=(int)cap*loadFactor,默認(rèn)情況下initialCapacity等于16,loadfactor等于0.75,通過(guò)運(yùn)算cap等于1,threshold等于零。
4,定位Segment
既然ConcurrentHashMap使用分段鎖Segment來(lái)保護(hù)不同段的數(shù)據(jù),那么在插入和獲取元素的時(shí)候,必須先通過(guò)哈希算法定位到Segment??梢钥吹紺oncurrentHashMap會(huì)首先使用Wang/Jenkins hash的變種算法對(duì)元素的hashCode進(jìn)行一次再哈希。
rivate static int hash(int h) { h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); }
之所以進(jìn)行再哈希,其目的是為了減少哈希沖突,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率。假如哈希的質(zhì)量差到極點(diǎn),那么所有的元素都在一個(gè)Segment中,不僅存取元素緩慢,分段鎖也會(huì)失去意義。我做了一個(gè)測(cè)試,不通過(guò)再哈希而直接執(zhí)行哈希計(jì)算。
1 System.out.println(Integer.parseInt("0001111", 2) & 15); 2 System.out.println(Integer.parseInt("0011111", 2) & 15); 3 System.out.println(Integer.parseInt("0111111", 2) & 15); 4 System.out.println(Integer.parseInt("1111111", 2) & 15);
計(jì)算后輸出的哈希值全是15,通過(guò)這個(gè)例子可以發(fā)現(xiàn)如果不進(jìn)行再哈希,哈希沖突會(huì)非常嚴(yán)重,因?yàn)橹灰臀灰粯樱瑹o(wú)論高位是什么數(shù),其哈希值總是一樣。我們?cè)侔焉厦娴亩M(jìn)制數(shù)據(jù)進(jìn)行再哈希后結(jié)果如下,為了方便閱讀,不足32位的高位補(bǔ)了0,每隔四位用豎線分割下。
1 0100|0111|0110|0111|1101|1010|0100|1110 2 1111|0111|0100|0011|0000|0001|1011|1000 3 0111|0111|0110|1001|0100|0110|0011|1110 4 1000|0011|0000|0000|1100|1000|0001|1010
可以發(fā)現(xiàn)每一位的數(shù)據(jù)都散列開(kāi)了,通過(guò)這種再哈希能讓數(shù)字的每一位都能參加到哈希運(yùn)算當(dāng)中,從而減少哈希沖突。ConcurrentHashMap通過(guò)以下哈希算法定位segment。
1 final Segment<K,V> segmentFor(int hash) { 2 return segments[(hash >>> segmentShift) & segmentMask]; 3 }
默認(rèn)情況下segmentShift為28,segmentMask為15,再哈希后的數(shù)最大是32位二進(jìn)制數(shù)據(jù),向右無(wú)符號(hào)移動(dòng)28位,意思是讓高4位參與到hash運(yùn)算中, (hash >>> segmentShift) & segmentMask的運(yùn)算結(jié)果分別是4,15,7和8,可以看到hash值沒(méi)有發(fā)生沖突。
5,ConcurrentHashMap的get操作
Segment的get操作實(shí)現(xiàn)非常簡(jiǎn)單和高效。先經(jīng)過(guò)一次再哈希,然后使用這個(gè)哈希值通過(guò)哈希運(yùn)算定位到segment,再通過(guò)哈希算法定位到元素,代碼如下:
1 public V get(Object key) { 2 int hash = hash(key.hashCode()); 3 return segmentFor(hash).get(key, hash); 4 }
get操作的高效之處在于整個(gè)get過(guò)程不需要加鎖,除非讀到的值是空的才會(huì)加鎖重讀,我們知道HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法里將要使用的共享變量都定義成volatile,如用于統(tǒng)計(jì)當(dāng)前Segement大小的count字段和用于存儲(chǔ)值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見(jiàn)性,能夠被多線程同時(shí)讀,并且保證不會(huì)讀到過(guò)期的值,但是只能被單線程寫(xiě)(有一種情況可以被多線程寫(xiě),就是寫(xiě)入的值不依賴(lài)于原值),在get操作里只需要讀不需要寫(xiě)共享變量count和value,所以可以不用加鎖。之所以不會(huì)讀到過(guò)期的值,是根據(jù)java內(nèi)存模型的happen before原則,對(duì)volatile字段的寫(xiě)入操作先于讀操作,即使兩個(gè)線程同時(shí)修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經(jīng)典應(yīng)用場(chǎng)景。
1 transient volatile int count; 2 volatile V value;
在定位元素的代碼里我們可以發(fā)現(xiàn)定位HashEntry和定位Segment的哈希算法雖然一樣,都與數(shù)組的長(zhǎng)度減去一相與,但是相與的值不一樣,定位Segment使用的是元素的hashcode通過(guò)再哈希后得到的值的高位,而定位HashEntry直接使用的是再哈希后的值。其目的是避免兩次哈希后的值一樣,導(dǎo)致元素雖然在Segment里散列開(kāi)了,但是卻沒(méi)有在HashEntry里散列開(kāi)。
1 hash >>> segmentShift) & segmentMask//定位Segment所使用的hash算法 2 int index = hash & (tab.length - 1);// 定位HashEntry所使用的hash算法
6,ConcurrentHashMap的Put操作
由于put方法里需要對(duì)共享變量進(jìn)行寫(xiě)入操作,所以為了線程安全,在操作共享變量時(shí)必須得加鎖。Put方法首先定位到Segment,然后在Segment里進(jìn)行插入操作。插入操作需要經(jīng)歷兩個(gè)步驟,第一步判斷是否需要對(duì)Segment里的HashEntry數(shù)組進(jìn)行擴(kuò)容,第二步定位添加元素的位置然后放在HashEntry數(shù)組里。
是否需要擴(kuò)容。在插入元素前會(huì)先判斷Segment里的HashEntry數(shù)組是否超過(guò)容量(threshold),如果超過(guò)閥值,數(shù)組進(jìn)行擴(kuò)容。值得一提的是,Segment的擴(kuò)容判斷比HashMap更恰當(dāng),因?yàn)镠ashMap是在插入元素后判斷元素是否已經(jīng)到達(dá)容量的,如果到達(dá)了就進(jìn)行擴(kuò)容,但是很有可能擴(kuò)容之后沒(méi)有新元素插入,這時(shí)HashMap就進(jìn)行了一次無(wú)效的擴(kuò)容。
如何擴(kuò)容。擴(kuò)容的時(shí)候首先會(huì)創(chuàng)建一個(gè)兩倍于原容量的數(shù)組,然后將原數(shù)組里的元素進(jìn)行再hash后插入到新的數(shù)組里。為了高效ConcurrentHashMap不會(huì)對(duì)整個(gè)容器進(jìn)行擴(kuò)容,而只對(duì)某個(gè)segment進(jìn)行擴(kuò)容。
7,ConcurrentHashMap的size操作
如果我們要統(tǒng)計(jì)整個(gè)ConcurrentHashMap里元素的大小,就必須統(tǒng)計(jì)所有Segment里元素的大小后求和。Segment里的全局變量count是一個(gè)volatile變量,那么在多線程場(chǎng)景下,我們是不是直接把所有Segment的count相加就可以得到整個(gè)ConcurrentHashMap大小了呢?不是的,雖然相加時(shí)可以獲取每個(gè)Segment的count的最新值,但是拿到之后可能累加前使用的count發(fā)生了變化,那么統(tǒng)計(jì)結(jié)果就不準(zhǔn)了。所以最安全的做法,是在統(tǒng)計(jì)size的時(shí)候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效。 因?yàn)樵诶奂觕ount操作過(guò)程中,之前累加過(guò)的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過(guò)不鎖住Segment的方式來(lái)統(tǒng)計(jì)各個(gè)Segment大小,如果統(tǒng)計(jì)的過(guò)程中,容器的count發(fā)生了變化,則再采用加鎖的方式來(lái)統(tǒng)計(jì)所有Segment的大小。
那么ConcurrentHashMap是如何判斷在統(tǒng)計(jì)的時(shí)候容器是否發(fā)生了變化呢?使用modCount變量,在put , remove和clean方法里操作元素前都會(huì)將變量modCount進(jìn)行加1,那么在統(tǒng)計(jì)size前后比較modCount是否發(fā)生變化,從而得知容器的大小是否發(fā)生變化。
到此這篇關(guān)于Java ConcurrentHashMap用法案例詳解的文章就介紹到這了,更多相關(guān)Java ConcurrentHashMap講解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jenkins節(jié)點(diǎn)配置實(shí)現(xiàn)原理及過(guò)程解析
這篇文章主要介紹了Jenkins節(jié)點(diǎn)配置實(shí)現(xiàn)原理及過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09淺談Servlet開(kāi)發(fā)技術(shù)基礎(chǔ)
這篇文章主要介紹了淺談Servlet開(kāi)發(fā)技術(shù)基礎(chǔ),具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12Java?20在Windows11系統(tǒng)下的簡(jiǎn)易安裝教程
這篇文章主要給大家介紹了關(guān)于Java?20在Windows11系統(tǒng)下的簡(jiǎn)易安裝教程,學(xué)習(xí)Java的同學(xué),第一步就是安裝好Java環(huán)境,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07Java的springcloud Sentinel是什么你知道嗎
這篇文章主要介紹了Java之springcloud Sentinel案例講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08spring boot 開(kāi)發(fā)soap webservice的實(shí)現(xiàn)代碼
這篇文章主要介紹了spring boot 開(kāi)發(fā)soap webservice的實(shí)現(xiàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-01-01Spring注解中@Autowired和@Bean的區(qū)別詳解
這篇文章主要詳細(xì)介紹了Spring注解中@Autowired和@Bean二者有什么區(qū)別,文中通過(guò)兩個(gè)注解的使用場(chǎng)景介紹了二者的區(qū)別,感興趣的同學(xué)可以參考閱讀2023-06-06Java static關(guān)鍵字詳細(xì)介紹與用法總結(jié)
這篇文章主要介紹了Java中static關(guān)鍵字的作用和用法詳細(xì)介紹,主要講了靜態(tài)方法、靜態(tài)變量、靜態(tài)類(lèi)、static和final一塊用等內(nèi)容。需要的朋友可以參考下2017-04-04