JDK并發(fā)容器簡介

Java提供的并發(fā)容器基本都在java.util.concurrent包中偶芍。比較常用的有ConcurrentHashMap、ConcurrentSkipListMap谅辣、CopyOnWriteArrayList凄吏、ConcurrentLinkedQueue铸敏、BlockingQueue等杈帐。

1体箕、線程安全的HashMap

HashMap是非線程安全的专钉,在并發(fā)環(huán)境中會(huì)帶來很多詭異的問題。

在多線程中使用線程安全的HashMap方式:

1)使用Collections.synchronizedMap()方法包裝HashMap

synchronizedMap方法
SynchronizedMap類的實(shí)現(xiàn)

SynchronizedMap類實(shí)現(xiàn)了Map累铅,通過mutex對象實(shí)現(xiàn)對m互斥操作跃须。其他所有相關(guān)的操作都有用到mutex對象進(jìn)行同步,從而實(shí)現(xiàn)線程安全娃兽。這種方式在多線程環(huán)境中的性能并不好菇民,所有對Map的操作都需要獲取mutex對象鎖,導(dǎo)致操作出現(xiàn)等待現(xiàn)象投储。

2)使用ConcurrentHashMap第练,性能更高。

2玛荞、線程安全的List

Java中ArrayList和Vector都是使用數(shù)組作為內(nèi)部實(shí)現(xiàn)的复旬,Vector是線程安全的,ArrayList不是冲泥。LinkedList使用鏈表的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了List,但它并非線程安全的壁涎,也可以通過Collections.synchronizedList()方式包裝List凡恍。

synchronizedList方法定義
SynchronizedList的實(shí)現(xiàn)

3、高效讀寫隊(duì)列ConcurrentLinkedQueue

JDK中提供了一個(gè)支持高并發(fā)的隊(duì)列ConcurrentLinkedQueue怔球,它使用鏈表結(jié)構(gòu)實(shí)現(xiàn)嚼酝。item表示目標(biāo)元素,next表示當(dāng)前Node的下一個(gè)元素竟坛。

Node節(jié)點(diǎn)定義

對Node操作使用了CAS操作闽巩,casItem()方法表示設(shè)置當(dāng)前Node的item值,它有2個(gè)參數(shù)担汤,第一個(gè)表示期望值涎跨,第二個(gè)為設(shè)置目標(biāo)值。若當(dāng)前值等于期望值cmp時(shí)崭歧,就會(huì)將目標(biāo)設(shè)為val隅很。casNext()也類似,它用來設(shè)置next字段率碾。

head和tail

head表示鏈表的頭部叔营,tail表示鏈表的尾部。head永遠(yuǎn)不會(huì)為null所宰,通過head和succ()方法能完整遍歷整個(gè)鏈表绒尊。

offer()向隊(duì)列中添加元素

offer()方法中無任何鎖操作,線程安全由CAS和隊(duì)列算法來保證仔粥。方法的核心是for循環(huán)婴谱,此循環(huán)沒有出口,直到嘗試成功為止。

當(dāng)首次加入元素時(shí)勘究,由于隊(duì)列為空矮湘,因此p.next=null,然后將p的next節(jié)點(diǎn)賦值為newNode口糕,即將新的元素加入到隊(duì)列中缅阳。此時(shí)p==t,如果casNext()執(zhí)行成功景描,查詢直接返回true十办。如果失敗則再進(jìn)行一次循環(huán),直到成功超棺。因此增加一個(gè)元素時(shí)向族,tail并不會(huì)被更新。

當(dāng)?shù)诙卧黾釉貢r(shí)棠绘,由于t還在head的位置上件相,p.next指向?qū)嶋H的第一個(gè)元素,因此q != null氧苍,表示q不是最后一個(gè)節(jié)點(diǎn)夜矗。由于往隊(duì)列中增加元素需要最后一個(gè)節(jié)點(diǎn)的位置,因此需要循環(huán)查找最后一個(gè)節(jié)點(diǎn)让虐。獲得最后一個(gè)節(jié)點(diǎn)后紊撕,p實(shí)際上指向鏈表的第一個(gè)元素,而它的next=null赡突,然后p更新自己的next对扶,讓它指向新加入的節(jié)點(diǎn)。如果成功則p != t惭缰,然后更新t所在位置浪南,將t移動(dòng)到鏈表最后。

如果p== q漱受,是由于遇到了哨兵(sentinel)節(jié)點(diǎn)導(dǎo)致的逞泄,哨兵節(jié)點(diǎn)就是next指向自己的節(jié)點(diǎn),主要表示要?jiǎng)h除的節(jié)點(diǎn)或空節(jié)點(diǎn)拜效。當(dāng)遇到哨兵節(jié)點(diǎn)時(shí)喷众,無法通過next獲取后續(xù)節(jié)點(diǎn),可能會(huì)直接返回head紧憾,并期望通過從鏈表頭部開始遍歷到千,再查找到鏈表末尾。

p = (t != (t =tail)) ? t :head;

在執(zhí)行?!=?時(shí)赴穗,線程會(huì)先取得t的值憔四,再執(zhí)行t = tail膀息,并取得新t的值,然后比較著兩個(gè)值是否相等了赵。在單線程下t !=?t?并不會(huì)成立潜支,但在并發(fā)下就有可能存在,如在獲得左邊的t值時(shí)柿汛,右邊的t值已被其他線程修改冗酿。如果在比較過程中,tail被其他線程修改络断,當(dāng)它再次賦值給t時(shí)裁替,會(huì)導(dǎo)致等式左右兩邊的t不同。如果兩個(gè)t不同貌笨,表示tail在中途被其他線程修改弱判。此時(shí)就可以用新的tail作為鏈表末尾,即等式右邊的t锥惋。如果tail沒有被修改昌腰,則返回head,要求從頭部開始重新查找尾部膀跌。

哨兵節(jié)點(diǎn)產(chǎn)生:當(dāng)隊(duì)列中只有一個(gè)節(jié)點(diǎn)時(shí)剥哑。

poll()定義

poll()方法用于彈出隊(duì)列內(nèi)的元素。

在updateHead()方法中淹父,將p作為新的鏈表頭部,原有的head被設(shè)置為哨兵(通過lazySetNext(h)方法實(shí)現(xiàn))怎虫。由于原有的head和tail實(shí)際上是同一元素暑认,因此再次用offer()添加元素時(shí),就會(huì)遇到這個(gè)哨兵tail大审。

4蘸际、高效讀取:CopyOnWriteArrayList

在大多數(shù)情況下徒扶,讀操作會(huì)遠(yuǎn)遠(yuǎn)大于寫操作粮彤,讀并不會(huì)修改原數(shù)據(jù),因此并沒有必要加鎖姜骡,讀是線程安全的导坟。當(dāng)寫操作時(shí)讀必須等待,否則可能讀到不一致的數(shù)據(jù)圈澈,如果正在讀時(shí)也不能進(jìn)行寫入惫周。

CopyOnWriteArrayList類的讀是不加鎖的,寫入也不會(huì)阻塞讀操作康栈,只有在寫寫的時(shí)候才進(jìn)行同步等待递递。其實(shí)現(xiàn)原理是:當(dāng)List需要修改時(shí)并不是修改的原內(nèi)容喷橙,而是對原有的數(shù)據(jù)進(jìn)行一份復(fù)制,將修改的數(shù)據(jù)寫入副本登舞,寫完后再將修改后的副本替換原有的數(shù)據(jù)贰逾。

get()

注意:讀操作代碼沒有任何同步控制和鎖操作,原因是內(nèi)部數(shù)組array不會(huì)發(fā)生修改菠秒,只會(huì)被另外array替換疙剑,可以保證數(shù)據(jù)安全。

add()

寫操作使用了鎖稽煤,Arrays.copyOf(elements, len +1)對內(nèi)部元素進(jìn)行了完整復(fù)制核芽,生成一個(gè)新的數(shù)組newElements,然后將新的元素加入newElements中酵熙,setArray(newElements)使用新的數(shù)組替換原有數(shù)組轧简,修改就完成了。此過程不會(huì)影響讀操作匾二,修改完成后哮独,讀線程可以立刻知道這個(gè)修改(因?yàn)閍rray是volatile的)。

5察藐、數(shù)據(jù)共享通道:BlockingQueue

BlockingQueue是一個(gè)接口皮璧,它適合作為數(shù)據(jù)共享的通道,它有如下實(shí)現(xiàn)類:

ArrayBlockingQueue

它的內(nèi)部元素都放在一個(gè)對象數(shù)組中:final Object[] items;

向隊(duì)列中壓入元素可以使用offer()和put()方法分飞。offer():如果當(dāng)前隊(duì)列已滿悴务,會(huì)立即返回false。如果未滿譬猫,則執(zhí)行入隊(duì)操作讯檐。put():也是將元素壓入到隊(duì)列末尾,如果隊(duì)列已滿染服,它會(huì)一直等待别洪,直到隊(duì)列中有空閑的位置。

從隊(duì)列中彈出元素可以使用poll()和take()方法柳刮,它們都是從隊(duì)列頭部獲取一個(gè)元素挖垛。區(qū)別是如果隊(duì)列為空時(shí)poll()會(huì)直接返回null,take()會(huì)等待秉颗,直到隊(duì)列內(nèi)有可用元素痢毒。

take()

當(dāng)執(zhí)行put()時(shí),如果隊(duì)列為空蚕甥,則讓當(dāng)前線程在notEmpty上等待闸准,新元素入隊(duì)時(shí),則進(jìn)行一次notEmpty上的通知梢灭。當(dāng)隊(duì)列中有新元素時(shí)夷家,線程會(huì)得到一個(gè)通知蒸其。當(dāng)新元素進(jìn)入隊(duì)列后,需要通知等待在notEmpty上的線程库快,讓它們繼續(xù)工作摸袁。

put()

對于put()操作也類似,當(dāng)隊(duì)列滿時(shí)需要讓壓入線程等待义屏,當(dāng)有元素從隊(duì)列中移走出現(xiàn)空位時(shí)靠汁,也需要通知等待入隊(duì)的線程。

6闽铐、隨機(jī)數(shù)據(jù)結(jié)構(gòu):跳表SkipList

跳表是一種可以用來快速查找的數(shù)據(jù)結(jié)構(gòu)蝶怔,類似于平衡樹。區(qū)別是對于平衡樹的插入和輸出可能導(dǎo)致平衡樹進(jìn)行一次全局的調(diào)整兄墅,而跳表只需要對整個(gè)數(shù)據(jù)結(jié)構(gòu)的局部進(jìn)行操作踢星。好處是在高并發(fā)情況下,需要一個(gè)全局鎖來保證整個(gè)平衡樹的線程安全隙咸,對于跳表只需要部分鎖即可沐悦,性能更好。

跳表的查詢時(shí)間復(fù)雜度為O(log n)五督。它采用隨機(jī)算法藏否,本質(zhì)是同時(shí)維護(hù)了多個(gè)鏈表,并且鏈表是分層的充包。最底層的鏈表維護(hù)了跳表內(nèi)的所有元素副签,每上面一層鏈表都是下面一層的子集,一個(gè)元素插入哪些層是完全隨機(jī)的基矮。

跳表內(nèi)的所有元素都是排序的淆储,查找時(shí),可以從頂級鏈表開始愈捅,一旦發(fā)現(xiàn)被查找的元素大于當(dāng)前鏈表中的取值,就會(huì)轉(zhuǎn)入下一層的鏈表繼續(xù)找慈鸠,在查找過程中搜索是跳躍式的蓝谨。跳表是一種使用空間換時(shí)間的算法。

使用跳表實(shí)現(xiàn)Map和使用哈希算法實(shí)現(xiàn)Map的不同之處是:哈希并不會(huì)保存元素的順序青团,而跳表內(nèi)所有元素都是排序的譬巫,在對跳表進(jìn)行遍歷時(shí),將會(huì)得到一個(gè)有序的結(jié)果督笆。

ConcurrentSkipListMap是使用跳表數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的芦昔。

ConcurrentSkipListMap使用示例

跳表的內(nèi)部數(shù)據(jù)結(jié)構(gòu)組成:Node和Index。

Node:一個(gè)Node表示一個(gè)節(jié)點(diǎn)娃肿,里面含有key咕缎、value珠十,每個(gè)Node還會(huì)指向下一個(gè)Node,即元素next凭豪。

Node定義
對Node的所有操作使用的CAS方法

casValue()方法用來設(shè)置value的值焙蹭,casNext()方法用來設(shè)置next字段。

Index:表示索引嫂伞,它內(nèi)部包裝了Node孔厉,同時(shí)增加了兩個(gè)向下的引用和向右的引用。整個(gè)跳表就是根據(jù)Index進(jìn)行全網(wǎng)組織的帖努。

Index定義

對于每一層的表頭撰豺,還需要記錄當(dāng)前處于那一層,因此還需要一個(gè)HeadIndex的數(shù)據(jù)結(jié)構(gòu)拼余,表示鏈表頭部的第一個(gè)Index污桦,它繼承于Index。

HeadIndex定義

總結(jié):對跳表的所有操作姿搜,就是組織好這些Index之間的連接關(guān)系寡润。


--參考文獻(xiàn)《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計(jì)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舅柜,隨后出現(xiàn)的幾起案子梭纹,更是在濱河造成了極大的恐慌,老刑警劉巖致份,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件变抽,死亡現(xiàn)場離奇詭異,居然都是意外死亡氮块,警方通過查閱死者的電腦和手機(jī)绍载,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滔蝉,“玉大人击儡,你說我怎么就攤上這事◎鹨” “怎么了阳谍?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長螃概。 經(jīng)常有香客問我矫夯,道長,這世上最難降的妖魔是什么吊洼? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任训貌,我火速辦了婚禮,結(jié)果婚禮上冒窍,老公的妹妹穿的比我還像新娘递沪。我一直安慰自己豺鼻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布区拳。 她就那樣靜靜地躺著拘领,像睡著了一般。 火紅的嫁衣襯著肌膚如雪樱调。 梳的紋絲不亂的頭發(fā)上约素,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音笆凌,去河邊找鬼圣猎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛乞而,可吹牛的內(nèi)容都是我干的送悔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼爪模,長吁一口氣:“原來是場噩夢啊……” “哼欠啤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屋灌,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤洁段,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后共郭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祠丝,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年除嘹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了写半。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尉咕,死狀恐怖叠蝇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情年缎,我是刑警寧澤悔捶,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站晦款,受9級特大地震影響炎功,放射性物質(zhì)發(fā)生泄漏枚冗。R本人自食惡果不足惜缓溅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赁温。 院中可真熱鬧坛怪,春花似錦淤齐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至居灯,卻和暖如春祭务,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怪嫌。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工义锥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人岩灭。 一個(gè)月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓拌倍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親噪径。 傳聞我的和親對象是個(gè)殘疾皇子柱恤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容