Java中的ConcurrentHashMap原理詳解
一、什么是ConcurrentHashMap
ConcurrentHashMap和HashMap一樣,是一個存放鍵值對的容器。
使用hash算法來獲取值的地址,因此時間復(fù)雜度是O(1)。查詢非???。 同時,ConcurrentHashMap是線程安全的HashMap。專門用于多線程環(huán)境。
二、ConcurrentHashMap和HashMap以及Hashtable的區(qū)別
2.1 HashMap
HashMap是線程不安全的,因為HashMap中操作都沒有加鎖,因此在多線程環(huán)境下會導(dǎo)致數(shù)據(jù)覆蓋之類的問題,所以,在多線程中使用HashMap是會拋出異常的。
2.2 HashTable
HashTable是線程安全的,但是HashTable只是單純的在put()方法上加上synchronized。保證插入時阻塞其他線程的插入操作。雖然安全,但因為設(shè)計簡單,所以性能低下。
2.3 ConcurrentHashMap
ConcurrentHashMap是線程安全的,ConcurrentHashMap并非鎖住整個方法,而是通過原子操作和局部加鎖的方法保證了多線程的線程安全,且盡可能減少了性能損耗。
由此可見,HashTable可真是一無是處…
三、ConcurrentHashMap原理
這一節(jié)專門介紹ConcurrentHashMap是如何保證線程安全的。如果想詳細(xì)了解ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu),請參考HashMap。
3.1 volatile修飾的節(jié)點數(shù)組
請看源碼
//ConcurrentHashMap使用volatile修飾節(jié)點數(shù)組,保證其可見性,禁止指令重排。 transient volatile Node<K,V>[] table;
再看看HashMap是怎么做的
//HashMap沒有用volatile修飾節(jié)點數(shù)組。 transient Node<K,V>[] table;
顯然,HashMap并不是為多線程環(huán)境設(shè)計的。
3.2 ConcurrentHashMap的put()方法
//put()方法直接調(diào)用putVal()方法 public V put(K key, V value) { return putVal(key, value, false); } //所以直接看putVal()方法。 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) tab = initTable(); 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 } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { 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) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
我來給大家講解一下步驟把。
public V put(K key, V value) {
首先,put()方法是沒有用synchronized修飾的。
for (Node<K,V>[] tab = table;;)
新插入一個節(jié)點時,首先會進(jìn)入一個死循環(huán), 情商高的就會說,這是一個樂觀鎖 進(jìn)入樂觀鎖后,
if (tab == null || (n = tab.length) == 0) tab = initTable();
如果tab未被初始化,則先將tab初始化。此時,這輪循環(huán)結(jié)束,因為被樂觀鎖鎖住,開始下一輪循環(huán)。 第二輪循環(huán),此時tab已經(jīng)被初始化了,所以跳過。
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 }
接下來通過key的hash值來判斷table中是否存在相同的key,如果不存在,執(zhí)行casTabAt()方法。 注意,這個操作時不加鎖的,看到里面的那行注釋了么// no lock when adding to empty bin。位置為空時不加鎖。 這里其實是利用了一個CAS操作。
CAS(Compare-And-Swap):比較并交換
這里就插播一個小知識,CAS就是通過一個原子操作,用預(yù)期值去和實際值做對比,如果實際值和預(yù)期相同,則做更新操作。 如果預(yù)期值和實際不同,我們就認(rèn)為,其他線程更新了這個值,此時不做更新操作。 而且這整個流程是原子性的,所以只要實際值和預(yù)期值相同,就能保證這次更新不會被其他線程影響。
好了,我們繼續(xù)。 既然這里用了CAS操作去更新值,那么就存在兩者情況。
- 實際值和預(yù)期值相同 相同時,直接將值插入,因為此時是線程安全的。好了,這時插入操作完成。使用break;跳出了樂觀鎖。循環(huán)結(jié)束。
- 實際值和預(yù)期值不同 不同時,不進(jìn)行操作,因為此時這個值已經(jīng)被其他線程修改過了,此時這輪操作就結(jié)束了,因為還被樂觀鎖鎖住,進(jìn)入第三輪循環(huán)。
第三輪循環(huán)中,前面的判斷又會重新執(zhí)行一次,我就跳過不說了,進(jìn)入后面的判斷。
else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);
這里判斷的是tab的狀態(tài),MOVED表示在擴(kuò)容中,如果在擴(kuò)容中,幫助其擴(kuò)容。幫助完了后就會進(jìn)行第四輪循環(huán)。 終于,來到了最后一輪循環(huán)。
else { V oldVal = null; synchronized (f) { 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) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } }
上面的判斷都不滿足時,就會進(jìn)入最后的分支,這條分支表示,key的hash值位置不為null(之前的判斷是hash值為null時直接做插入操作),表示發(fā)生了hash沖突,此時節(jié)點就要通過鏈表的形式存儲這個插入的新值。Node類是有next字段的,用來指向鏈表的下一個位置,新節(jié)點就往這插。
synchronized (f) {
看,終于加排它鎖了,只有在發(fā)生hash沖突的時候才加了排它鎖。
if (tabAt(tab, i) == f) { if (fh >= 0) {
重新判斷當(dāng)前節(jié)點是不是第二輪判斷過的節(jié)點,如果不是,表示節(jié)點被其他線程改過了,進(jìn)入下一輪循環(huán), 如果是,再次判斷是否在擴(kuò)容中,如果是,進(jìn)入下一輪循環(huán), 如果不是,其他線程沒改過,繼續(xù)走,
for (Node<K,V> e = f;; ++binCount) {
for循環(huán),循環(huán)遍歷這個節(jié)點上的鏈表,
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; }
找到一個hash值相同,且key也完全相同的節(jié)點,更新這個節(jié)點。 如果找不到
if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; }
往鏈表最后插入這個新節(jié)點。因為在排他鎖中,這些操作都可以直接操作。終于到這插入就基本完成了。
總結(jié)
做插入操作時,首先進(jìn)入樂觀鎖, 然后,在樂觀鎖中判斷容器是否初始化, 如果沒初始化則初始化容器, 如果已經(jīng)初始化,則判斷該hash位置的節(jié)點是否為空,如果為空,則通過CAS操作進(jìn)行插入。 如果該節(jié)點不為空,再判斷容器是否在擴(kuò)容中,如果在擴(kuò)容,則幫助其擴(kuò)容。 如果沒有擴(kuò)容,則進(jìn)行最后一步,先加鎖,然后找到hash值相同的那個節(jié)點(hash沖突), 循環(huán)判斷這個節(jié)點上的鏈表,決定做覆蓋操作還是插入操作。 循環(huán)結(jié)束,插入完畢。
3.3 ConcurrentHashMap的get()方法
//ConcurrentHashMap的get()方法是不加鎖的,方法內(nèi)部也沒加鎖。 public V get(Object key)
看上面這代碼,ConcurrentHashMap的get()方法是不加鎖的,為什么可以不加鎖?因為table有volatile關(guān)鍵字修飾,保證每次獲取值都是最新的。
//Hashtable的get()是加鎖的,所以性能差。 public synchronized V get(Object key)
再看看Hashtable,差距啊。
四、使用場景
嗯,多線程環(huán)境下,更新少,查詢多時使用的話,性能比較高。
樂觀鎖嘛,認(rèn)為更新操作時不會被其他線程影響。
所以時候再更新少的情況下性能高。
到此這篇關(guān)于Java中的ConcurrentHashMap原理詳解的文章就介紹到這了,更多相關(guān)ConcurrentHashMap原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA 非常重要的一些設(shè)置項(一連串的問題差點讓我重新用回 Eclipse)
這篇文章主要介紹了IDEA 非常重要的一些設(shè)置項(一連串的問題差點讓我重新用回 Eclipse),本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08spring aop底層源碼執(zhí)行邏輯剖析(源碼解析)
這篇文章主要介紹了spring aop底層源碼執(zhí)行邏輯剖析,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-08-08Spring Boot開發(fā)Web應(yīng)用詳解
這篇文章主要介紹了Spring Boot開發(fā)Web應(yīng)用詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04SpringBoot實用小技巧之如何動態(tài)設(shè)置日志級別
這篇文章主要給大家介紹了關(guān)于SpringBoot實用小技巧之如何動態(tài)設(shè)置日志級別的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用SpringBoot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Mybatis中collection和association的使用區(qū)別詳解
這篇文章主要介紹了Mybatis中collection和association的使用區(qū)別詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能詳解
這篇文章主要介紹了Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能,較為詳細(xì)的講述了橋接模式的概念、原理并結(jié)合實例形式分析了Java使用橋接模式實現(xiàn)開關(guān)和電燈照明功能相關(guān)操作步驟與注意事項,需要的朋友可以參考下2018-05-05