日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

concurrenthashmap_ConcurrentHashMap是如何保证线程安全的

發(fā)布時(shí)間:2023/12/3 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 concurrenthashmap_ConcurrentHashMap是如何保证线程安全的 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章已同步發(fā)表于微信公眾號(hào)JasonGaoH,ConcurrentHashMap是如何保證線程安全的

之前分析過(guò)HashMap的一些實(shí)現(xiàn)細(xì)節(jié),關(guān)于HashMap你需要知道的一些細(xì)節(jié), 今天我們從源碼角度來(lái)看看ConcurrentHashMap是如何實(shí)現(xiàn)線程安全的,其實(shí)網(wǎng)上這類(lèi)文章分析特別多,秉著”紙上得來(lái)終覺(jué)淺,絕知此事要躬行“的原則,我們嘗試自己去分析下,希望這樣對(duì)于ConcurrentHashMap有一個(gè)更深刻的理解。

為什么說(shuō)HashMap線程不安全,而ConcurrentHashMap就線程安全

其實(shí)ConcurrentHashMap在Android開(kāi)發(fā)中使用的場(chǎng)景并不多,但是ConcurrentHashMap為了支持多線程并發(fā)這些優(yōu)秀的設(shè)計(jì)卻是最值得我們學(xué)習(xí)的地方,往往”ConcurrentHashMap是如何實(shí)現(xiàn)線程安全“這類(lèi)問(wèn)題卻是面試官比較喜歡問(wèn)的問(wèn)題。

首先,我們嘗試用代碼模擬下HashMap在多線程場(chǎng)景下會(huì)不安全,如果把這個(gè)場(chǎng)景替換成ConcurrentHashMap會(huì)不會(huì)有問(wèn)題。

因?yàn)椴煌谄渌木€程同步問(wèn)題,想模擬出一種場(chǎng)景來(lái)表明HashMap是線程不安全的稍微有點(diǎn)麻煩,可能是hash散列有關(guān),在數(shù)據(jù)量較小的情況下,計(jì)算出來(lái)的hashCode是不太容易產(chǎn)生碰撞的,網(wǎng)上很多文章都是嘗試從源碼角度來(lái)分析HashMap可能會(huì)導(dǎo)致的線程安全問(wèn)題。

我們來(lái)看下下面這段代碼,我們構(gòu)造10個(gè)線程,每個(gè)線程分別往map中put 1000個(gè)數(shù)據(jù),為了保證每個(gè)數(shù)據(jù)的key不一樣,我們將i+ 線程名字來(lái)作為map 的key,這樣,如果所有的線程都累加完的話,我們預(yù)期的map的size應(yīng)該是10 * 1000 = 10000。

import?java.util.HashMap;import?java.util.Map;import?java.util.concurrent.ConcurrentHashMap;public?class?HashMapTest?{?public?static?void?main(String[]?args)?{??Map?map?=?new?HashMap();??//?????Map?map?=?new?ConcurrentHashMap();??for?(int?i?=?0;?i??1)???Thread.yield();????System.out.println(map.size());?}}class?MyThread?extends?Thread?{????public?Map?map;????public?String?name;????public?MyThread(Map?map,?String?name)?{??????this.map?=?map;??????this.name?=?name;????}????public?void?run()?{?????for(int?i?=0;i<1000;i++)?{??????map.put(i?+?name,?i?+?name);?????}????}??}

使用HashMap,程序運(yùn)行,結(jié)果如下:

9930

那我們?nèi)绻堰@里的HashMap換成ConcurrentHashMap來(lái)試試看看效果如何,輸出結(jié)果如下:

10000

我們發(fā)現(xiàn)不管運(yùn)行幾次,HashMap的size都是小于10000的,而ConcurrentHashMap的size都是10000。從這個(gè)角度也證明了ConcurrentHashMap是線程安全的,而HashMap則是線程不安全的。 HashMap在多線程put的時(shí)候,當(dāng)產(chǎn)生hash碰撞的時(shí)候,會(huì)導(dǎo)致丟失數(shù)據(jù),因?yàn)橐猵ut的兩個(gè)值hash相同,如果這個(gè)對(duì)于hash桶的位置個(gè)數(shù)小于8,那么應(yīng)該是以鏈表的形式存儲(chǔ),由于沒(méi)有做通過(guò),后面put的元素可能會(huì)直接覆蓋之前那個(gè)線程put的數(shù)據(jù),這樣就導(dǎo)致了數(shù)據(jù)丟失。

其實(shí)列舉上面這個(gè)例子只是為了從一個(gè)角度來(lái)展示下為什么說(shuō)HashMap線程不安全,而ConcurrentHashMap則是線程安全的,鑒于HashMap線程安全例子比較難列舉出來(lái),所以才通過(guò)打印size這個(gè)角度來(lái)模擬了下。

這篇文章深入解讀HashMap線程安全性問(wèn)題就詳細(xì)介紹了HashMap可能會(huì)出現(xiàn)線程安全問(wèn)題。 文章主要講了兩個(gè)可能會(huì)出現(xiàn)線程不安全地方,一個(gè)是多線程的put可能導(dǎo)致元素的丟失,另一個(gè)是put和get并發(fā)時(shí),可能導(dǎo)致get為null,但是也僅是在源碼層面分析了下,因?yàn)檫@種場(chǎng)景想要完全用代碼展示出來(lái)是稍微有點(diǎn)麻煩的。

接下來(lái)我們來(lái)看看ConcurrentHashMap是如何做到線程安全的。

JDK8的ConcurrentHashMap文檔提煉

  • ConcurrentHashMap支持檢索的完全并發(fā)和更新的高預(yù)期并發(fā)性,這里的說(shuō)法很有意思檢索支持完全并發(fā),更新則支持高預(yù)期并發(fā)性,因?yàn)樗臋z索操作是沒(méi)有加鎖的,實(shí)際上檢索也沒(méi)有必要加鎖。
  • 實(shí)際上ConcurrentHashMap和Hashtable在不考慮實(shí)現(xiàn)細(xì)節(jié)來(lái)說(shuō),這兩者完全是可以互相操作的,Hashtable在get,put,remove等這些方法中全部加入了synchronized,這樣的問(wèn)題是能夠?qū)崿F(xiàn)線程安全,但是缺點(diǎn)是性能太差,幾乎所有的操作都加鎖的,但是ConcurrentHashMap的檢測(cè)操作卻是沒(méi)有加鎖的。
  • ConcurrentHashMap檢索操作(包括get)通常不會(huì)阻塞,因此可能與更新操作(包括put和remove)重疊。
  • ConcurrentHashMap跟Hashtable類(lèi)似但不同于HashMap,它不可以存放空值,key和value都不可以為null。

印象中一直以為ConcurrentHashMap是基于Segment分段鎖來(lái)實(shí)現(xiàn)的,之前沒(méi)仔細(xì)看過(guò)源碼,一直有這么個(gè)錯(cuò)誤的認(rèn)識(shí)。ConcurrentHashMap是基于Segment分段鎖來(lái)實(shí)現(xiàn)的,這句話也不能說(shuō)不對(duì),加個(gè)前提條件就是正確的了,ConcurrentHashMap從JDK1.5開(kāi)始隨java.util.concurrent包一起引入JDK中,在JDK8以前,ConcurrentHashMap都是基于Segment分段鎖來(lái)實(shí)現(xiàn)的,在JDK8以后,就換成synchronized和CAS這套實(shí)現(xiàn)機(jī)制了。

JDK1.8中的ConcurrentHashMap中仍然存在Segment這個(gè)類(lèi),而這個(gè)類(lèi)的聲明則是為了兼容之前的版本序列化而存在的。

???/**?????*?Stripped-down?version?of?helper?class?used?in?previous?version,?????*?declared?for?the?sake?of?serialization?compatibility.?????*/????static?class?Segment?extends?ReentrantLock?implements?Serializable?{????????private?static?final?long?serialVersionUID?=?2249069246763182397L;????????final?float?loadFactor;????????Segment(float?lf)?{?this.loadFactor?=?lf;?}????}

JDK1.8中的ConcurrentHashMap不再使用Segment分段鎖,而是以table數(shù)組的頭結(jié)點(diǎn)作為synchronized的鎖。和JDK1.8中的HashMap類(lèi)似,對(duì)于hashCode相同的時(shí)候,在Node節(jié)點(diǎn)的數(shù)量少于8個(gè)時(shí),這時(shí)的Node存儲(chǔ)結(jié)構(gòu)是鏈表形式,時(shí)間復(fù)雜度為O(N),當(dāng)Node節(jié)點(diǎn)的個(gè)數(shù)超過(guò)8個(gè)時(shí),則會(huì)轉(zhuǎn)換為紅黑樹(shù),此時(shí)訪問(wèn)的時(shí)間復(fù)雜度為O(long(N))。

?/**?????*?The?array?of?bins.?Lazily?initialized?upon?first?insertion.?????*?Size?is?always?a?power?of?two.?Accessed?directly?by?iterators.?????*/????transient?volatile?Node[]?table;

數(shù)據(jù)結(jié)構(gòu)圖如下所示:

其實(shí)ConcurrentHashMap保證線程安全主要有三個(gè)地方。

一、使用volatile保證當(dāng)Node中的值變化時(shí)對(duì)于其他線程是可見(jiàn)的

二、使用table數(shù)組的頭結(jié)點(diǎn)作為synchronized的鎖來(lái)保證寫(xiě)操作的安全

三、當(dāng)頭結(jié)點(diǎn)為null時(shí),使用CAS操作來(lái)保證數(shù)據(jù)能正確的寫(xiě)入。

使用volatile

可以看到,Node中的val和next都被volatile關(guān)鍵字修飾。

volatile的happens-before規(guī)則:對(duì)一個(gè)volatile變量的寫(xiě)一定可見(jiàn)(happens-before)于隨后對(duì)它的讀。

也就是說(shuō),我們改動(dòng)val的值或者next的值對(duì)于其他線程是可見(jiàn)的,因?yàn)関olatile關(guān)鍵字,會(huì)在讀指令前插入讀屏障,可以讓高速緩存中的數(shù)據(jù)失效,重新從主內(nèi)存加載數(shù)據(jù)。

static?class?Node?implements?Map.Entry?{????????final?int?hash;????????final?K?key;????????volatile?V?val;????????volatile?Node?next;??}??...

另外,ConcurrentHashMap提供類(lèi)似tabAt來(lái)讀取Table數(shù)組中的元素,這里是以volatile讀的方式讀取table數(shù)組中的元素,主要通過(guò)Unsafe這個(gè)類(lèi)來(lái)實(shí)現(xiàn)的,保證其他線程改變了這個(gè)數(shù)組中的值的情況下,在當(dāng)前線程get的時(shí)候能拿到。

?static?final??Node?tabAt(Node[]?tab,?int?i)?{????????return?(Node)U.getObjectVolatile(tab,?((long)i?<

而與之對(duì)應(yīng)的,是setTabAt,這里是以volatile寫(xiě)的方式往數(shù)組寫(xiě)入元素,這樣能保證修改后能對(duì)其他線程可見(jiàn)。

?static?final??void?setTabAt(Node[]?tab,?int?i,?Node?v)?{????????U.putObjectVolatile(tab,?((long)i?<

我們來(lái)看下ConcurrentHashMap的putVal方法:

??/**?Implementation?for?put?and?putIfAbsent?*/????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[]?tab?=?table;;)?{????????????Node?f;?int?n,?i,?fh;????????????if?(tab?==?null?||?(n?=?tab.length)?==?0)????????????????tab?=?initTable();????????????//當(dāng)頭結(jié)點(diǎn)為null,則通過(guò)casTabAt方式寫(xiě)入????????????else?if?((f?=?tabAt(tab,?i?=?(n?-?1)?&?hash))?==?null)?{????????????????if?(casTabAt(tab,?i,?null,?????????????????????????????new?Node(hash,?key,?value,?null)))????????????????????break;???????????????????//?no?lock?when?adding?to?empty?bin????????????}????????????else?if?((fh?=?f.hash)?==?MOVED)??????????????//正在擴(kuò)容????????????????tab?=?helpTransfer(tab,?f);????????????else?{????????????????V?oldVal?=?null;????????????????//頭結(jié)點(diǎn)不為null,使用synchronized加鎖????????????????synchronized?(f)?{????????????????????if?(tabAt(tab,?i)?==?f)?{????????????????????????if?(fh?>=?0)?{????????????????????????????//此時(shí)hash桶是鏈表結(jié)構(gòu)????????????????????????????binCount?=?1;????????????????????????????for?(Node?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?pred?=?e;????????????????????????????????if?((e?=?e.next)?==?null)?{????????????????????????????????????pred.next?=?new?Node(hash,?key,??????????????????????????????????????????????????????????????value,?null);????????????????????????????????????break;????????????????????????????????}????????????????????????????}????????????????????????}????????????????????????else?if?(f?instanceof?TreeBin)?{????????????????????????????//此時(shí)是紅黑樹(shù)????????????????????????????Node?p;????????????????????????????binCount?=?2;????????????????????????????if?((p?=?((TreeBin)f).putTreeVal(hash,?key,???????????????????????????????????????????????????????????value))?!=?null)?{????????????????????????????????oldVal?=?p.val;????????????????????????????????if?(!onlyIfAbsent)????????????????????????????????????p.val?=?value;????????????????????????????}????????????????????????}????????????????????????else?if?(f?instanceof?ReservationNode)????????????????????????????throw?new?IllegalStateException("Recursive?update");????????????????????}????????????????}????????????????if?(binCount?!=?0)?{????????????????????//當(dāng)鏈表結(jié)構(gòu)大于等于8,則將鏈表轉(zhuǎn)換為紅黑樹(shù)????????????????????if?(binCount?>=?TREEIFY_THRESHOLD)????????????????????????treeifyBin(tab,?i);????????????????????if?(oldVal?!=?null)??????????????????return?oldVal;????????????????????break;????????????????}????????????}????????}????????addCount(1L,?binCount);????????return?null;????}

在putVal方法重要的地方都加了注釋,可以幫助理解,現(xiàn)在我們一步一步來(lái)看putVal方法。

使用CAS

當(dāng)有一個(gè)新的值需要put到ConcurrentHashMap中時(shí),首先會(huì)遍歷ConcurrentHashMap的table數(shù)組,然后根據(jù)key的hashCode來(lái)定位到需要將這個(gè)value放到數(shù)組的哪個(gè)位置。

tabAt(tab, i = (n - 1) & hash))就是定位到這個(gè)數(shù)組的位置,如果當(dāng)前這個(gè)位置的Node為null,則通過(guò)CAS方式的方法寫(xiě)入。所謂的CAS,即即compareAndSwap,執(zhí)行CAS操作的時(shí)候,將內(nèi)存位置的值與預(yù)期原值比較,如果相匹配,那么處理器會(huì)自動(dòng)將該位置值更新為新值,否則,處理器不做任何操作。

這里就是調(diào)用casTabAt方法來(lái)實(shí)現(xiàn)的。

?????static?final??boolean?casTabAt(Node[]?tab,?int?i,????????????????????????????????????????Node?c,?Node?v)?{????????return?U.compareAndSwapObject(tab,?((long)i?<

casTabAt同樣是通過(guò)調(diào)用Unsafe類(lèi)來(lái)實(shí)現(xiàn)的,調(diào)用Unsafe的compareAndSwapObject來(lái)實(shí)現(xiàn),其實(shí)如果仔細(xì)去追蹤這條線路,會(huì)發(fā)現(xiàn)其實(shí)最終調(diào)用的是cmpxchg這個(gè)CPU指令來(lái)實(shí)現(xiàn)的,這是一個(gè)CPU的原子指令,能保證數(shù)據(jù)的一致性問(wèn)題。

使用synchronized

當(dāng)頭結(jié)點(diǎn)不為null時(shí),則使用該頭結(jié)點(diǎn)加鎖,這樣就能多線程去put hashCode相同的時(shí)候不會(huì)出現(xiàn)數(shù)據(jù)丟失的問(wèn)題。synchronized是互斥鎖,有且只有一個(gè)線程能夠拿到這個(gè)鎖,從而保證了put操作是線程安全的。

下面是ConcurrentHashMap的put操作的示意圖,圖片來(lái)自于ConcurrentHashMap源碼分析(JDK8)get/put/remove方法分析

參考文章

從ConcurrentHashMap的演進(jìn)看Java多線程核心技術(shù)

ConcurrentHashMap源碼分析(JDK8)get/put/remove方法分析

總結(jié)

以上是生活随笔為你收集整理的concurrenthashmap_ConcurrentHashMap是如何保证线程安全的的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。