《Java并發(fā)編程的藝術》學習筆記

????本文是我自己在秋招復習時的讀書筆記粮彤,整理的知識點根穷,也是為了防止忘記,尊重勞動成果导坟,轉載注明出處哦屿良!如果你也喜歡,那就點個小心心惫周,文末贊賞一杯豆奶吧尘惧,嘻嘻。 讓我們共同成長吧……


第1章? 并發(fā)編程的挑戰(zhàn)

????????并發(fā)編程的目的是讓程序運行得更快递递,但是并不是啟動更多的線程就能讓程序最大限度地并發(fā)執(zhí)行喷橙。并發(fā)編程會遇到許多挑戰(zhàn),例如:上下文切換問題登舞、死鎖問題贰逾、以及首受限于硬件和軟件的資源限制問題。

1.1? 上下文切換

? ???? 進行上下文切換之前菠秒,會保存上一個任務的狀態(tài)疙剑,以便下次切換回這個任務時,可以再加載到這個狀態(tài)。任務從保存到再加載的過程就是一次上下文切換言缤。

? ? 1嚼蚀、多線程一定快嗎?

? ? ? ? ????不一定轧简。因為創(chuàng)建線程和上下文切換會占用一定的時間驰坊。

? ? 2、如何減少上下文切換

? ? ????????方法:無鎖并發(fā)編程哮独、CAS算法、使用最少線程察藐、使用協(xié)程皮璧。

1.2? 死鎖

? ? 常見避免死鎖方法:

? ????? 1)避免一個線程同時獲得多個鎖;

? ? ????2)避免一個線程在鎖內同時占用多個資源分飞,盡量保證每個鎖只占用一個資源悴务;

? ????? 3)嘗試使用定時鎖,使用lock.tryLock(timeout)來代替使用內部鎖

? ? ????4)對于數(shù)據(jù)庫鎖譬猫,加鎖和解鎖必須在一個數(shù)據(jù)庫連接里讯檐,否則會出現(xiàn)解鎖失敗情況。

1.3? 資源限制的挑戰(zhàn)

? ? ????資源限制:指的是在進行并發(fā)編程時染服,程序的執(zhí)行速度受限于計算機硬件資源或軟件資源别洪。

1.4? 本章小結


第2章? Java并發(fā)機制的底層實現(xiàn)原理

? ? ????Java代碼在編譯后變成字節(jié)碼,字節(jié)碼被類加載器加載到JVM中柳刮,JVM執(zhí)行字節(jié)碼挖垛,最終轉換為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機制依賴于JVM的實現(xiàn)和CPU的指令秉颗。

2.1? volatile的應用

????????在并發(fā)編程中synchronized和volatile都具有重要的作用痢毒,volatile是輕量級的synchronized,保證了共享變量的可見性蚕甥。volatile使用得當哪替,會比synchronized的使用和執(zhí)行成本更低,因為volatile不會引起線程上下文切換和調度菇怀。

? ??1凭舶、volatile的定義和實現(xiàn)原理

? ??????在Java語言規(guī)范中對volatile的定義如下:Java編程語言中允許線程訪問共享變量,為了確保共享變量能被準確和一致的更新敏释,線程應該確保通過排他鎖來確保單獨獲取這個變量库快。Java提供了volatile,在某些情況下比鎖更加方便钥顽。

? ? ? ? 與volatile實現(xiàn)相關的CPU術語:內存屏障(memory barriers)义屏、緩沖行(cache line)、原子操作(atomic operations)、緩沖行填充(cache line fill)闽铐、緩沖命中(cache hit)蝶怔、寫命中(write hit)、寫缺失(write misses the cache)

? ? ? ? volatile如何保證可見性兄墅?被volatile修飾的共享變量進行寫操作時會多出加了lock的匯編代碼踢星,lock指令在多核處理器下會依法2件事情:

? ? ? ? 1)將當前處理器緩沖行的數(shù)據(jù)寫回到系統(tǒng)內存

? ? ? ? 2)這個寫回內存操作會使在其他CPU里緩存了該內存地址的數(shù)據(jù)無效

? ? ? ? 在多處理器下,為了保證各個處理器的緩存一致性隙咸,就會實現(xiàn)緩存一致性協(xié)議沐悦,每個處理器通過嗅探總線上傳播的數(shù)據(jù)來檢查自己緩存是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應的內存地址被修改五督,就將當前處理器的緩存行設置為無效狀態(tài)藏否,當處理器對這個數(shù)據(jù)進行修改操作時,會重新從內存系統(tǒng)中把數(shù)據(jù)讀到處理器緩存里充包。

????????volatile的兩條實現(xiàn)原則:

????????1)Lock前綴指令會引起處理器緩存寫回到內存副签。

? ? ? ? 2)一個處理器的緩存寫回到內存會導致其他處理器的緩存無效

? ? 2、volatile的使用優(yōu)化

? ? ????JDK 7的并發(fā)包中新增一個隊列集合類LinkedTransferQueue,它在使用volatile變量時基矮,用一種追加字節(jié)的方式來優(yōu)化隊列的出隊淆储、入隊的性能。LinkedTransferQueue里的PaddedAtomicRefernce內部類只做了1件事家浇,就是講volatile共享變量追加到64字節(jié)(一個對象引用占4字節(jié)本砰、追加15個變量就是60字節(jié),加上父類的value變量蓝谨,一共64字節(jié))灌具。對于64位的處理器,追加64字節(jié)能提高并發(fā)編程的效率譬巫,因為64位處理器不支持部分填充緩沖行咖楣,這就意味著,如果隊列的頭結點和尾結點不足64字節(jié)芦昔,處理器會將它們讀取到一個緩沖行中诱贿,再多處理器下的每個處理器都會緩存同樣的頭、尾結點咕缎,當一個處理器鎖定緩沖行進行修改時珠十,那么在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾結點凭豪,導致隊列的出隊入隊效率低下焙蹭。追加字節(jié)后,避免了頭結點和尾結點在一個緩沖行中嫂伞,可以使得頭尾節(jié)點修改時不會相互鎖定孔厉。

? ? 當緩存行不是64字節(jié)寬的處理器拯钻;共享變量不會頻繁地寫,就不需要追加到64字節(jié)撰豺。

2.2? synchronized的實現(xiàn)原理與應用

? ??????synchronized是重量級鎖粪般,在Java SE 1.6 對synchronized進行了各種優(yōu)化,就沒那么重了污桦。Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗問題而引入了偏向鎖亩歹、輕量級鎖等優(yōu)化措施。

? ??????synchronized實現(xiàn)同步的基礎:Java中的每一個對象都可以作為鎖凡橱。具體表現(xiàn)形式:對于普通同步方法小作,鎖是當前實例對象;對于靜態(tài)同步方法梭纹,鎖是當前類的Class對象躲惰;對于同步代碼塊,鎖是synchronized括號里配置的對象变抽。當一個線程試圖訪問同步代碼塊/同步方法時,必須先得到鎖氮块,退出或者拋出異常時必須釋放鎖绍载。

? ??synchronized在JVM的實現(xiàn)原理是:JVM基于進入和退出Monitor對象實現(xiàn)方法或者代碼塊同步。但是二者實現(xiàn)細節(jié)不一樣滔蝉。同步代碼塊是使用monitorenter和monitorexit指令實現(xiàn)击儡,而同步方法時其他方式。但是同步方法也可以使用這兩個指令實現(xiàn)蝠引。

? ? 2.2.1? Java對象頭

????????synchronized用的鎖是存在Java對象頭里的阳谍。如果對象是數(shù)組類型,則虛擬機用3個字寬存儲對象頭螃概,非數(shù)組類型使用2字寬存儲對象頭矫夯。1寬等于4字節(jié)。

? ? Java對象頭:Mark Word(存對象的hashCode吊洼、分代年齡闷畸、鎖信息等)臊泌、Class Metadata Address、Array Length

? ? 2.2.2? 鎖的升級與對比

????????在JavaSE1.6中,鎖的狀態(tài)有:無鎖狀態(tài)蕾哟、偏向鎖狀態(tài)、輕量級鎖狀態(tài)薛窥、重量級鎖狀態(tài)味混。鎖可以升級,但是不可以降級谬莹,目的是為了提高獲得鎖和釋放鎖的效率檩奠。

? ? 1? 偏向鎖

? ? ? ?在大多的情況下桩了,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得笆凌,為了讓線程獲得鎖的代價更低圣猎,引入了偏向鎖?。當一個線程訪問同步塊并獲取鎖時乞而,會在對象頭和棧幀的鎖記錄里邊存儲偏向鎖的線程ID送悔,以后該線程在進入和退出同步塊是不需要進行CAS操作來加解鎖,只需要測試一下對象頭中的Mark Word中是否存儲著指向該線程的偏向鎖爪模。若是測試成功欠啤,表示線程已經(jīng)獲得了鎖;若是測試不成功屋灌,則需要在測試一下Mark Word中偏向鎖的標志是否設置為1(表示當前已經(jīng)處于偏向鎖狀態(tài)):若是沒有設置洁段,則使用CAS鎖競爭機制來競爭鎖;若是設置了共郭,則嘗試使用CAS將對象頭的偏向鎖指向當前線程祠丝。

? ? ?(1)偏向鎖的撤銷

? ? ? ? 偏向鎖使用一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其它線程嘗試競爭偏向鎖時除嘹,持有偏向鎖的線程才會釋放写半。

? ? ? ? 偏向鎖的撤銷,需要等到全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)尉咕。它首先會暫停擁有偏向鎖的線程叠蝇,然后檢查持有偏向鎖的線程是否活著,如果不處于活動狀態(tài)年缎,則將對象頭設置成無鎖狀態(tài)悔捶;如果線程仍然活著,擁有偏向鎖的棧會被執(zhí)行单芜,遍歷偏向對象的鎖記錄蜕该,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖缓溅,最后喚醒暫停的線程蛇损。

? ? ?(2)關閉偏向鎖

? ? Java 6和Java7默認是啟動偏向鎖的。是在應用程序啟動后幾秒鐘才激活坛怪∮倨耄可以使用 -XX:BiasedLockingStartupDelay=0設置立即啟動⊥嗄洌可以使用 -XX:-UsebiasedLocking-false關閉偏向鎖更啄,那么程序默認進入輕量級鎖。

? ? 2? 輕量級鎖

? ? (1)輕量級鎖加鎖

? ??????線程在執(zhí)行同步塊之前居灯,JVM會先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間祭务,并將對象頭中的Mark Word復制到鎖記錄中内狗,官方稱為“Displaced Mark Word”。然后線程嘗試使用CAS將對象頭中的MarkWord替換為指向鎖記錄的指針义锥。如果成功柳沙,當前線程獲得鎖,如果失敗拌倍,表示其他線程競爭鎖赂鲤,當前線程便嘗試使用自旋來獲取鎖。

? ? (2)輕量級鎖解鎖

????????輕量級解鎖時柱恤,會使用原子的CAS造作將Displaced Mark Word替換回到對象頭数初,如果成功,則表示沒有發(fā)生競爭梗顺。如果失敗泡孩,表示當前鎖存在競爭,鎖會膨脹為重量級鎖寺谤。

????????因為自旋會消耗CPU仑鸥,為了避免無用的自旋(獲得鎖的線程被阻塞了),一旦鎖升級成重量級鎖变屁,就不會再恢復到輕量級鎖狀態(tài)锈候。當鎖處于這個狀態(tài)下時,其他線程試圖獲取鎖是都會被阻塞敞贡,當持有鎖的線程釋放鎖之后會喚醒這些線程。

????3摄职、鎖的優(yōu)缺點對比

2.3? 原子操作的實現(xiàn)原理

? ??原子操作也就是說這個操作是不可以在進行細分的誊役,必須一次性全部執(zhí)行完成,不可以執(zhí)行一部分之后被中斷去執(zhí)行另一個操作谷市。

? ? 1蛔垢、相關術語

? ? 緩存行(cache line)、比較并交換(Compare and Swap)迫悠、CPU流水線(CPU pipeline)鹏漆、內存順序沖突

? ? 2、處理器是如何實現(xiàn)原子操作的

? ??????對于處理器而言创泄,原子操作也就是說在同一時間只能有一個處理器對數(shù)據(jù)進行處理艺玲,而且這個操作是原子性的,不可分割的鞠抑。常見的有兩種實現(xiàn)方式:一種是通過總線鎖來保證原子性饭聚,另一種是通過緩存鎖來保證原子性。

? ??????總線鎖:總線鎖就是使用處理器提供的一個LOCK#信號搁拙,當一個處理器在總線上輸出此信號時秒梳,其他處理器的請求將被阻塞住法绵,這樣發(fā)出信號的處理器就可以獨占內存,保證操作的原子性酪碘。

? ??????緩存鎖:緩存鎖是為了優(yōu)化總線鎖而設計出來的朋譬。因為總線鎖在被鎖住期間,其他的處理器是無法處理其他的數(shù)據(jù)的兴垦,只能等待鎖釋放開徙赢。但是若是對緩存進行加鎖就可以減少這個影響。他是指內存區(qū)域如果被緩存在處理器的緩存行中滑进,并且咋Lock操作期間被鎖定犀忱,那么當他執(zhí)行鎖操作寫回到內存時,處理器直接修改內部的內存地址扶关,并允許他的緩存一致性機制來保證操作的原子性阴汇,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區(qū)域數(shù)據(jù)。

? ? 3节槐、Java如何實現(xiàn)原子操作

? ??????????在Java中采用了循環(huán)CAS的方式來保證原子操作

? ? ? ? ? ?(1)?循環(huán)CAS:JVM的CAS操作正式利用了處理器提供的CMPXCHG指令實現(xiàn)的搀庶。基本思路就是通過循環(huán)進行CAS操作直到成功為止铜异。

? ? ????????(2)CAS實現(xiàn)原子操作的三大問題

? ? ? ? ? ? ? ? ABA問題(JDK1.5使用AtomicStampedReference解決ABA問題)哥倔、循環(huán)時間長開銷大、只能保證一個共享變量的原子操作

? ? ? ? ? ? ?(3)鎖機制:鎖機制保證只有獲得鎖的線程才能操作鎖定的內存區(qū)域揍庄,但是Java中的多個鎖咆蒿,除了偏向鎖以外,JVM實現(xiàn)鎖的方式都是循環(huán)CAS蚂子,即當一個線程想進入同步塊時使用循環(huán)CAS獲取所沃测;當退出同步塊時使用循環(huán)CAS釋放鎖。

2.4? 本章小結


第3章? Java內存模型

????本節(jié)分為4部分:

? ? Java內存模型基礎:介紹內存模型相關的基本概念

????Java內存模型中的順序一致性介紹重排序與順序一致性內存模型

????同步原語:介紹synchronized食茎、volatile蒂破、final的內存語義以及重排序規(guī)則在處理器中的實現(xiàn)

????Java內存模型的設計:介紹Java內存模型的設計原理以及其與處理器內存模型和順序一致性內存模型的關系。

3.1? Java內存模型的基礎

????3.1.1? 并發(fā)編程模型的兩個關鍵問題

? ? ? ? 先發(fā)編程中需要處理兩個關鍵問題:線程之間如何通信别渔;線程之間如何同步

????????通信:指線程之間以何種機制來交換信息附迷。線程之間的通信機制:共享內存、消息傳遞

????????同步:指程序中用于控制不同線程間操作發(fā)生相對順序的機制哎媚。

? ? 3.1.2? Java內存模型的抽象結構

? ? ? ? Java線程間的通信由JMM控制喇伯,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本抄伍。本地內存是JMM的一個抽象概念,并不真實存在艘刚。它涵蓋了緩存、寫緩沖區(qū)截珍、寄存器以及其他的硬件和編譯器優(yōu)化攀甚。

Java內存模型的抽象結構示意圖

? ??????如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個步驟箩朴。

  ????1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。

  ????2)線程B到主內存中去讀取線程A之前已更新過的共享變量秋度。

????3.1.3? 從源代碼到指令序列的重排序

? ??????在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序炸庞。重排序分3種類型:

????????1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序荚斯。

????????2)指令級并行的重排序〔壕樱現(xiàn)代處理器采用了指令級并行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序事期。

????????3)內存系統(tǒng)的重排序滥壕。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。

從源代碼到最終執(zhí)行的指令序列示意圖

? ? 上圖的1屬于編譯器重排序,2和3屬于處理器重排序兽泣。這些重排序可能會導致多線程程序出現(xiàn)內存可見性問題绎橘。對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序唠倦。

? ? JMM屬于語言級的內存模型称鳞,它確保在不同的編譯器和處理器上,通過禁止特定類型的重排序稠鼻,為程序員提供一致的內存可見性保證冈止。

????3.1.4? 并發(fā)編程模型的分類

? ? ????為了保證內存可見性,Java編譯器在生成指令序列的適當位置插入內存屏障指令來禁止特定類型的處理器重排序候齿。JMM內存屏障有4類:


????屏障類型? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?說明

????LoadLoad Barriers? ? ? ? ? ? ? ? ? 確保Load1數(shù)據(jù)的裝載先于Load2及所有后序裝置指令的裝載

????StoreStore Barriers? ? ? ? ? ? ? ? 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內存)先于Store2及所有后序存儲指令的存儲

????LoadStore Barriers? ? ? ? ? ? ? ? ?確保Load1數(shù)據(jù)裝載先于Store2及所有后序的存儲指令刷新到內存

????StoreLoad Barriers? ? ? ? ? 確保Store1數(shù)據(jù)對其他處理器變得可見(指刷新到內存)先于Load2及所有后序裝載指令的裝載熙暴。StoreLoad Barriers

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 會使改屏障之前的所有內存訪問指令(存儲和裝載指令)完成之后,才執(zhí)行該屏障之后的內存訪問指令


? ??StoreLoad Barriers 具有其他3中屏障效果慌盯。

3.1.5? happens-before簡介

? ? JDK 5開始怨咪,Java使用新的JSR-133內存模型,該模型使用happens-before來闡述操作之間的內存可見性润匙。在JMM中,一個操作執(zhí)行的結果需要對另外一個操作可見唉匾,那么這兩個操作之間必須存在happens-before關系孕讳。

????1)程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作鲤桥。

????2)監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖锹引。

????3)volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。

????4)傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C痒给。

????5)start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作峡懈。

????6)join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回璃饱。

? ? 7)線程終止規(guī)則

????8)對象終結規(guī)則

3.2? 重排序

? ? 重排序:指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。

? ? 3.2.1? 數(shù)據(jù)依賴

? ??????如果兩個操作訪問同一個變量肪康,且這兩個操作中有一個為寫操作荚恶,此時這兩個操作之間就存在數(shù)據(jù)依賴性撩穿。數(shù)據(jù)依賴分下列三種類型:

數(shù)據(jù)依賴類型表

????????上面三種情況,只要重排序兩個操作的執(zhí)行順序谒撼,程序的執(zhí)行結果將會被改變食寡。編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性廓潜,編譯器和處理器不會改變存在數(shù)據(jù)依賴關系的兩個操作的執(zhí)行順序抵皱。

????????注意,這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作辩蛋,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮呻畸。

?????3.2.2? as-if-serial語義

? ??????as-if-serial語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結果不能被改變悼院。編譯器伤为,runtime 和處理器都必須遵守as-if-serial語義。

????????為了遵守as-if-serial語義樱蛤,編譯器和處理器不會對存在數(shù)據(jù)依賴關系的操作做重排序钮呀,因為這種重排序會改變執(zhí)行結果。但是昨凡,如果操作之間不存在數(shù)據(jù)依賴關系爽醋,這些操作可能被編譯器和處理器重排序。

????3.2.3? 程序順序規(guī)則

????3.2.4? 重排序對多線程的影響

3.3? 順序一致性

????3.3.1? 數(shù)據(jù)競爭與順序一致性

? ??????當程序未正確同步時便脊,就會存在數(shù)據(jù)競爭蚂四。java內存模型規(guī)范對數(shù)據(jù)競爭的定義如下:在一個線程中寫一個變量,在另一個線程讀同一個變量哪痰,而且寫和讀沒有通過同步來排序遂赠。

????????JMM對正確同步的多線程程序的內存一致性做了如下保證:如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(sequentially consistent)–即程序的執(zhí)行結果與該程序在順序一致性內存模型中的執(zhí)行結果相同晌杰。這里的同步是指廣義上的同步跷睦,包括對常用同步原語(synchronized,volatile和final)的正確使用肋演。

????3.3.2順序一致性內存模型

????????順序一致性內存模型有兩大特性:一個線程中的所有操作必須按照程序的順序來執(zhí)行抑诸;(不管程序是否同步)所有線程都只能看到一個單一的操作執(zhí)行順序。在順序一致性內存模型中爹殊,每個操作都必須原子執(zhí)行且立刻對所有線程可見蜕乡。

????????在概念上,順序一致性模型有一個單一的全局內存梗夸,這個內存通過一個左右擺動的開關可以連接到任意一個線程层玲。同時,每一個線程必須按程序的順序來執(zhí)行內存讀/寫操作。在任意時間點最多只能有一個線程可以連接到內存辛块。當多個線程并發(fā)執(zhí)行時畔派,圖中的開關裝置能把所有線程的所有內存讀/寫操作串行化。

????3.3.3同步程序的順序一致性效果

? ??????在順序一致性模型中憨降,所有操作完全按程序的順序串行執(zhí)行父虑。而在JMM中,臨界區(qū)內的代碼可以重排序(但JMM不允許臨界區(qū)內的代碼“逸出”到臨界區(qū)之外授药,那樣會破壞監(jiān)視器的語義)士嚎。JMM會在退出監(jiān)視器和進入監(jiān)視器這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖(具體細節(jié)后文會說明)悔叽。雖然線程A在臨界區(qū)內做了重排序莱衩,但由于監(jiān)視器的互斥執(zhí)行的特性,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內的重排序娇澎。這種重排序既提高了執(zhí)行效率笨蚁,又沒有改變程序的執(zhí)行結果。從這里我們可以看到JMM在具體實現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結果的前提下趟庄,盡可能的為編譯器和處理器的優(yōu)化打開方便之門括细。

????3.3.4未同步程序的執(zhí)行特性

????????JMM不保證未同步程序的執(zhí)行結果與該程序在順序一致性模型中的執(zhí)行結果一致。因為未同步程序在順序一致性模型中執(zhí)行時戚啥,整體上是無序的奋单,其執(zhí)行結果無法預知。保證未同步程序在兩個模型中的執(zhí)行結果一致毫無意義猫十。

????????JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性览濒,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性。

????????在計算機中拖云,數(shù)據(jù)通過總線在處理器和內存之間傳遞贷笛。每次處理器和內存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(bus transaction)宙项》啵總線事務包括讀事務(read transaction)和寫事務(write transaction)。讀事務從內存?zhèn)魉蛿?shù)據(jù)到處理器尤筐,寫事務從處理器傳送數(shù)據(jù)到內存邑贴,每個事務會讀/寫內存中一個或多個物理上連續(xù)的字。這里的關鍵是叔磷,總線會同步試圖并發(fā)使用總線的事務。在一個處理器執(zhí)行總線事務期間奖磁,總線會禁止其它所有的處理器和I/O設備執(zhí)行內存的讀/寫改基。下面讓我們通過一個示意圖來說明總線的工作機制:

????????總線的這些工作機制可以把所有處理器對內存的訪問以串行化的方式來執(zhí)行;在任意時間點,最多只能有一個處理器能訪問內存秕狰。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性稠腊。

????????在一些32位的處理器上,如果要求對64位數(shù)據(jù)的寫操作具有原子性鸣哀,會有比較大的開銷架忌。為了照顧這種處理器,java語言規(guī)范鼓勵但不強求JVM對64位的long型變量和double型變量的寫具有原子性我衬。當JVM在這種處理器上運行時叹放,會把一個64位long/ double型變量的寫操作拆分為兩個32位的寫操作來執(zhí)行。這兩個32位的寫操作可能會被分配到不同的總線事務中執(zhí)行挠羔,此時對這個64位變量的寫將不具有原子性井仰。從JSR -133內存模型開始(即從JDK5開始),僅僅只允許把一個64位long/ double型變量的寫操作拆分為兩個32位的寫操作來執(zhí)行破加,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個讀事務中執(zhí)行)俱恶。

3.4? volatile的內存語義

????3.4.1? volatile的特性

????????可見性。對一個volatile變量的讀范舀,總是能看到(任意線程)對這個volatile變量最后的寫入合是。

????????原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性锭环。

????3.4.2? volatile寫-讀建立的happens-before關系

? ??????從內存語義的角度來說聪全,volatile寫和鎖的釋放有相同的內存語義,相當于退出同步塊田藐;volatile讀與鎖的獲取有相同的內存語義荔烧,相當于進入同步代碼塊。

????3.4.3? volatile 寫-讀的內存語義

????????volatile寫的內存語義如下:

????????????當寫一個volatile變量時汽久,JMM會把該線程對應的本地內存中的共享變量刷新到主內存鹤竭。

? ?? ??volatile讀的內存語義如下:

????????????當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效景醇。線程接下來將從主內存中讀取共享變量臀稚。

? ??????下面對volatile寫和volatile讀的內存語義做個總結:

????????????線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發(fā)出了(其對共享變量所在修改的)消息三痰。

????????????線程B讀一個volatile變量吧寺,實質上是線程B接收了之前某個線程發(fā)出的(在寫這個volatile變量之前對共享變量所做修改的)消息。

????????????線程A寫一個volatile變量散劫,隨后線程B讀這個volatile變量稚机,這個過程實質上是線程A通過主內存向線程B發(fā)送消息。

????3.4.4? volatile內存語義的實現(xiàn)

????????前文我們提到過重排序分為編譯器重排序和處理器重排序获搏。為了實現(xiàn)volatile內存語義赖条,JMM會分別限制這兩種類型的重排序類型。下面是JMM針對編譯器制定的volatile重排序規(guī)則表:

3.4.5??JSR-133為什么要增強volatile的內存語義

? ? ????JSR-133專家組決定增強volatile的內存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取一樣纬乍,具有相同的內存語義碱茁。從編譯器重排序規(guī)則和處理器內存屏障插入策略來看,只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語意仿贬,這種重排序就會被編譯器重排序規(guī)則和處理器內存屏障插入策略禁止纽竣。

????????由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖的互斥執(zhí)行的特性可以確保對整個臨界區(qū)代碼的執(zhí)行具有原子性茧泪。在功能上蜓氨,鎖比volatile更強大;在可伸縮性和執(zhí)行性能上调炬,volatile更有優(yōu)勢语盈。這里說下使用volatile的條件:

????????對變量的寫不依賴于變量的當前值,訪問變量是不需要加鎖缰泡。

3.5? 鎖的內存語義

3.5.1? 鎖的釋放-獲取建立的happens-before關系

????鎖除了可以讓臨界區(qū)互斥執(zhí)行外刀荒,還可讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息。

3.5.2? 鎖的釋放-獲取的內存語義

? ??當線程獲取鎖時棘钞,JMM會把該線程對應的本地內存置為無效缠借。

????當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中宜猜。

? ? 對比鎖的釋放-獲取的內存語義與volatile寫-讀的內存語義可以看出:鎖的釋放與volatile的寫具有相同的內存語義泼返;鎖的獲取與volatile的讀具有相同的內存語義。

????下面對鎖的釋放和鎖的獲取的內存語義做個總結:

????????????線程A釋放一個鎖姨拥,實質上是線程A向接下來將要獲得這個鎖的某個線程發(fā)出了(線程A對共享變量所在修改的)消息绅喉。

????????????線程B獲取一個鎖,實質上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息叫乌。

????????????線程A釋放鎖柴罐,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發(fā)送消息憨奸。

3.5.3? 鎖內存語義的實現(xiàn)??

? ? 借助ReentrantLock源代碼革屠,來分析鎖內存語義的集體實現(xiàn)機制。ReentrantLock

3.5.4? cncurrent包的實現(xiàn)

? ? ????Java的CAS會使用現(xiàn)代處理器上提供的高效機器級別原子指令排宰,這些原子指令以原子方式對內存執(zhí)行讀-改-寫操作似芝,這是在多處理器中實現(xiàn)同步的關鍵。同時板甘,volatile變量的讀/寫和CAS可以實現(xiàn)線程之間的通信党瓮。把這些特性整合在一起,就形成了整個concurrent包得以實現(xiàn)的基石盐类。

????如果我們仔細分析concurrent包的源代碼實現(xiàn)寞奸,會發(fā)現(xiàn)一個通用化的實現(xiàn)模式:

????????首先般渡,聲明共享變量為volatile嗦明;

????????然后疏叨,使用CAS的原子條件更新來實現(xiàn)線程之間的同步寡喝;

????????同時踊沸,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現(xiàn)線程之間的通信臭觉。

????AQS颜及,非阻塞數(shù)據(jù)結構和原子變量類(java.util.concurrent.atomic包中的類)祸憋,這些concurrent包中的基礎類都是使用這種模式來實現(xiàn)的吐咳,而concurrent包中的高層類又是依賴于這些基礎類來實現(xiàn)的逻悠。從整體來看,concurrent包的實現(xiàn)示意圖如下:

3.6? final域的內存語義

? ? 3.6.1? final域的重排序規(guī)則

? ??對于final域韭脊,編譯器和處理器要遵守兩個重排序規(guī)則:

? ? ? ? 1)在構造函數(shù)內對一個final域的寫入童谒,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序沪羔。

? ? ? ? 2) 初次讀一個包含final域的對象的引用饥伊,與隨后初次讀這個final域,這兩個操作之間不能重排序蔫饰。

????3.6.2?寫final域的重排序規(guī)則

????????寫final域的重排序規(guī)則禁止把final域的寫重排序到構造函數(shù)之外琅豆。這個規(guī)則的實現(xiàn)包含下面2個方面:

? ? ? ????? 1)JMM禁止編譯器把final域的寫重排序到構造函數(shù)之外。

? ? ? ? ????2)編譯器會在final域的寫之后篓吁,構造函數(shù)返回之前茫因,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數(shù)之外杖剪。

? ??????寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前冻押,對象的final域已經(jīng)被正確初始化過了,而普通域不具有這個保障盛嘿。

????3.6.3? 讀final域的重排序規(guī)則

????????讀final域的重排序規(guī)則如下:

????????在一個線程中洛巢,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意孩擂,這個規(guī)則僅僅針對處理器)狼渊。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。

? ??????讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前类垦,一定會先讀包含這個final域的對象的引用狈邑。

? ? 3.6.4??final域是引用類型

????????對于引用類型,寫final域的重排序規(guī)則對編譯器和處理器增加了如下約束:在構造函數(shù)內對一個final引用的對象的成員域的寫入蚤认,與隨后在構造函數(shù)外把這個被構造對象的引用賦值給一個引用變量米苹,這兩個操作之間不能重排序。

? ? 3.6.5? 為什么final引用不能從構造函數(shù)內“溢出”

? ??????前面我們提到過砰琢,寫final域的重排序規(guī)則可以確保:在引用變量為任意線程可見之前蘸嘶,該引用變量指向的對象的final域已經(jīng)在構造函數(shù)中被正確初始化過了良瞧。其實要得到這個效果,還需要一個保證:在構造函數(shù)內部训唱,不能讓這個被構造對象的引用為其他線程可見褥蚯,也就是對象引用不能在構造函數(shù)中“逸出”。

? ? 3.6.6??final語義在處理器中的實現(xiàn)

????????現(xiàn)在我們以x86處理器為例况增,說明final語義在處理器中的具體實現(xiàn)赞庶。

????????上面我們提到,寫final域的重排序規(guī)則會要求譯編器在final域的寫之后澳骤,構造函數(shù)return之前歧强,插入一個StoreStore障屏。讀final域的重排序規(guī)則要求編譯器在讀final域的操作前面插入一個LoadLoad屏障为肮。

????????由于x86處理器不會對寫-寫操作做重排序摊册,所以在x86處理器中,寫final域需要的StoreStore障屏會被省略掉颊艳。同樣茅特,由于x86處理器不會對存在間接依賴關系的操作做重排序,所以在x86處理器中籽暇,讀final域需要的LoadLoad屏障也會被省略掉温治。也就是說在x86處理器中,final域的讀/寫不會插入任何內存屏障戒悠!

3.7? happens-before

? ? 3.7.1? JMM的設計

? ? ? ? 為了一方面要為程序員提供足夠強的內存可見性保證熬荆;另一方面對編譯器和處理器的限制要盡可能的放松,設計JMM時需要進行平衡绸狐。JMM把happens- before要求禁止的重排序分為了下面兩類:

? ? ? ? 1)會改變程序執(zhí)行結果的重排序卤恳。

? ? ? ? 2)不會改變程序執(zhí)行結果的重排序。

????JMM對這兩種不同性質的重排序寒矿,采取了不同的策略:

? ? ? ? 1)對于會改變程序執(zhí)行結果的重排序突琳,JMM要求編譯器和處理器必須禁止這種重排序。

? ? ? ? 2)對于不會改變程序執(zhí)行結果的重排序符相,JMM對編譯器和處理器不作要求(JMM允許這種重排序)拆融。

????下面是JMM的設計示意圖:

JMM的設計示意圖

? ??從上圖可以看出兩點:

????????JMM向程序員提供的happens- before規(guī)則能滿足程序員的需求。JMM的happens- before規(guī)則不但簡單易懂啊终,而且也向程序員提供了足夠強的內存可見性保證(有些內存可見性保證其實并不一定真實存在镜豹,比如上面的A happens- before B)。

????????JMM對編譯器和處理器的束縛已經(jīng)盡可能的少蓝牲。從上面的分析我們可以看出趟脂,JMM其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行例衍。比如昔期,如果編譯器經(jīng)過細致的分析后已卸,認定一個鎖只會被單個線程訪問,那么這個鎖可以被消除硼一。再比如累澡,如果編譯器經(jīng)過細致的分析后,認定一個volatile變量僅僅只會被單個線程訪問般贼,那么編譯器可以把這個volatile變量當作一個普通變量來對待永乌。這些優(yōu)化既不會改變程序的執(zhí)行結果,又能提高程序的執(zhí)行效率具伍。

????3.7.2 ?happens-before的定義

? ???happens-before的定義:

? ? ? ? 1)如果一個操作?happens-before另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見圈驼,而且第一個操作的執(zhí)行順序排在第二個操作之前人芽。

? ? ? ? 2)兩個操作之間存在?happens-before關系,并不意味著Java平臺的具體實現(xiàn)必須要按照?happens-before關系指定的順序執(zhí)行绩脆。只要重排序的執(zhí)行結果與按照?happens-before執(zhí)行的一致萤厅,是可以的。

????3.7.3 ?happens-before 規(guī)則

? ? ? ? 8個規(guī)則靴迫,不再重復

3.8??雙重檢查鎖定與優(yōu)化

? ? 在Java多線程程序中惕味,有時候需要延遲初始化來降低初始化類或創(chuàng)建對象的開銷。雙重檢查鎖定是常見的延遲初始化的技術玉锌,但它是一個錯誤的用法名挥。

? ? 錯誤根源:例如使用雙重檢查鎖定實現(xiàn)單例模式,當?shù)谝淮闻袛鄬嵗粸閚ull時主守,很可能instance引用的對象還沒初始化完成禀倔。因為instance=new Singleton()創(chuàng)建一個對象,可以分解為如下3行偽代碼:

? ? ? ?1参淫、 memory=allocate(); //分配對象的內存空間

? ? ? ?2救湖、 ctorInstance(memory); //初始化對象

? ? ? ?3、 instance=memory;//設置instance指向剛剛分配的內存地址

? ? 上述偽代碼2 涎才、3 之間很可能重排序鞋既。

? ? 解決辦法:基于volatile的解決方案;基于靜態(tài)內部類初始化解決方案

3.9? Java內存模型綜述

? ? 3.9.1 處理器內存模型

????順序一致性內存模型是一個理論參考模型耍铜,JMM和處理器內存模型在設計時通常會把順序一致性內存模型作為參照邑闺。JMM和處理器內存模型在設計時會對順序一致性模型做一些放松,因為如果完全按照順序一致性模型來實現(xiàn)處理器和JMM业扒,那么很多的處理器和編譯器優(yōu)化都要被禁止检吆,這對執(zhí)行性能將會有很大的影響。

????由于常見的處理器內存模型比JMM要弱程储,java編譯器在生成字節(jié)碼時蹭沛,會在執(zhí)行指令序列的適當位置插入內存屏障來限制處理器的重排序臂寝。JMM屏蔽了不同處理器內存模型的差異,它在不同的處理器平臺之上為java程序員呈現(xiàn)了一個一致的內存模型摊灭。

????3.9.2 JMM的設計

????3.9.3JMM的內存可見性保證

????????Java程序的內存可見性保證按程序類型可以分為下列三類:

????????單線程程序咆贬。單線程程序不會出現(xiàn)內存可見性問題。編譯器帚呼,runtime和處理器會共同確保單線程程序的執(zhí)行結果與該程序在順序一致性模型中的執(zhí)行結果相同掏缎。

????????正確同步的多線程程序。正確同步的多線程程序的執(zhí)行將具有順序一致性(程序的執(zhí)行結果與該程序在順序一致性內存模型中的執(zhí)行結果相同)煤杀。這是JMM關注的重點眷蜈,JMM通過限制編譯器和處理器的重排序來為程序員提供內存可見性保證。

????????未同步/未正確同步的多線程程序沈自。JMM為它們提供了最小安全性保障:線程執(zhí)行時讀取到的值酌儒,要么是之前某個線程寫入的值,要么是默認值(0枯途,null忌怎,false)。

只要多線程程序是正確同步的酪夷,JMM保證該程序在任意的處理器平臺上的執(zhí)行結果榴啸,與該程序在順序一致性內存模型中的執(zhí)行結果一致。

????3.9.4 JSR-133對舊內存模型的修補

????????JSR-133對JDK5之前的舊內存模型的修補主要有兩個:

????????增強volatile的內存語義晚岭。舊內存模型允許volatile變量與普通變量重排序鸥印。JSR-133嚴格限制volatile變量與普通變量的重排序,使volatile的寫-讀和鎖的釋放-獲取具有相同的內存語義坦报。

????????增強final的內存語義辅甥。在舊內存模型中,多次讀取同一個final變量的值可能會不相同燎竖。為此璃弄,JSR-133為final增加了兩個重排序規(guī)則。現(xiàn)在构回,final具有了初始化安全性夏块。


第4章? Java并發(fā)編程基礎

4.1? 線程簡介

? ? 4.1.1? 什么是線程

? ????? 線程是輕量級進程,一個進程可以創(chuàng)建多個線程纤掸,各個線程擁有各自的計數(shù)器脐供、堆棧和局部變量等屬性。

? ? 4.1.2? 為什么要使用多線程

????????原因有:更多的處理器核心借跪;更快的響應速度政己;更好的編程模型

? ? 4.1.3? 線程優(yōu)先級

? ? ????在java中,優(yōu)先級范圍:1~10掏愁,可以通過setPriority(int)修改優(yōu)先級歇由,默認是5.優(yōu)先級高的線程分配時間片的數(shù)量多卵牍。

? ? 4.1.4? 線程狀態(tài)

? ? ? ? Java線程的狀態(tài):NEW、RUNNABLE沦泌、BLOCKED糊昙、WAITING、TIME_WAITING谢谦、TERMINATED

? ? 4.1.5? Daemon線程

? ? 可以通過Thread.setDaemon(true)設置释牺。

4.2? 啟動和終止線程

? ? 4.2.1? 構造線程

????????繼承Thread類,實現(xiàn)Runnable接口回挽、Callable接口? ? ? ??

? ? 4.2.2? 啟動線程

? ? ? ? 調用start()方法

? ? 4.2.3? 理解中斷

? ? ? ? 線程可以通過調用其它線程的interrupt()對其他線程進行終端操作鉴裹。

? ? ? ? 線程可以通過isInterrupted()判斷是否被中斷

? ? ? ? 也可以通過靜態(tài)方法Thread.interrupted()對當前線程的中斷標識位進行復位儒老。

? ? 4.2.4? 過期的suspend()讹堤、resume()和stop()

? ? 4.2.5? 安全地終止線程

4.3? 線程間通信

????4.3.1 volatile和synchronized關鍵字

????????線程開始運行戈二,擁有自己的棧空間队塘,Java支持多個線程同時訪問一個對象或者對象的成員變量,由于每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內存是在共享內存中的宜鸯,但是每個執(zhí)行的線程還是可以擁有一份拷貝憔古,這樣做的目的是加速程序的執(zhí)行,這是現(xiàn)代多核處理器的一個顯著特性)淋袖,所以程序在執(zhí)行過程中鸿市,一個線程看到的變量并不一定是最新的。

????????關鍵字volatile可以用來修飾字段(成員變量)即碗,就是告知程序任何對該變量的訪問均需要從共享內存中獲取焰情,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性剥懒。

????????關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用内舟,它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中初橘,它保證了線程對變量訪問的可見性和排他性验游。

????????同步代碼就不在貼出來,對于同步塊的實現(xiàn)使用了monitorenter和monitorexit指令保檐,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的耕蝉。無論采用哪種方式,其本質是對一個對象的監(jiān)視器(monitor)進行獲取夜只,而這個獲取過程是排他的垒在,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器。

????????任意一個對象都擁有自己的監(jiān)視器扔亥,當這個對象由同步塊或者這個對象的同步方法調用時场躯,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法谈为,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態(tài)推盛。

????????從圖4-2中可以看到峦阁,任意線程對Object(Object由synchronized保護)的訪問,首先要獲得Object的監(jiān)視器耘成。如果獲取失敗榔昔,線程進入同步隊列,線程狀態(tài)變?yōu)锽LOCKED瘪菌。當訪問Object的前驅(獲得了鎖的線程)釋放了鎖撒会,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監(jiān)視器的獲取师妙。

4.3.2等待/通知機制

? ? ? 這個機制背景就是為了解耦生產(chǎn)者诵肛、消費者的問題,簡單的辦法是使用輪詢默穴,但是輪詢缺點是及時性怔檩、性能不能保證,所以采用通知機制避免輪詢帶來的性能損失蓄诽。?

????????等待/通知的相關方法是任意Java對象都具備的薛训,因為這些方法被定義在所有對象的超類java.lang.Object上,方法和描述如表4-2所示仑氛。

????等待/通知機制乙埃,是指一個線程A調用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調用了對象O的notify()或者notifyAll()方法锯岖,線程A收到通知后從對象O的wait()方法返回介袜,進而執(zhí)行后續(xù)操作。上述兩個線程通過對象O來完成交互出吹,而對象上的wait()和notify/notifyAll()的關系就如同開關信號一樣遇伞,用來完成等待方和通知方之間的交互工作。

? ?4.3.3等待/通知的經(jīng)典范式

????從上節(jié)的示例中可以提取經(jīng)典范式捶牢,分為等待方(消費者)和通知方(生產(chǎn)者)

????等待方:

????????1)獲取對象鎖赃额;

????????2)如果條件不滿足,那么調用對象的wait()方法叫确,被通知后仍要檢查條件跳芳。

????????3)條件滿足則執(zhí)行對應的邏輯。

????對應的偽代碼如下:

? ??通知方:

????????????1)獲得對象的鎖竹勉;

????????????2)改變條件飞盆;

????????????3)通知所有等待在對象上的線程。

????對應的偽代碼如下:

4.3.4 管道的輸入、輸出流

????管道的輸入吓歇、輸出流主要用于線程間的數(shù)據(jù)傳輸孽水,傳輸?shù)拿浇闉閮却妗?/p>

????這塊只是淡出提了下,屬于nio的范疇城看,需單獨整理女气,例子就不貼了。

4.3.5? Thread.join的使用

? ? ?如果一個線程A執(zhí)行了thread.join()語句测柠,其含義是:當前線程A等待thread線程終止之后才從thread.join()返回炼鞠。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(longmillis,int nanos)兩個具備超時特性的方法轰胁。這兩個超時方法表示谒主,如果線程thread在給定的超時時間里沒有終止,那么將會從該超時方法中返回赃阀。

4.3.6 ThreadLocal的使用

????作者只是舉例演示使用方式霎肯,這里注意使用場景,具體參見這篇文章:ThreadLock.

4.4線程應用實例

????這里不細寫了榛斯,作者分別介紹了數(shù)據(jù)庫連接池示例观游、線程池技術、基于線程池的簡單web服務器驮俗《疲可以參照原書去理解。


第5章? Java中的鎖

5.1? Lock接口

? ? 在Lock出現(xiàn)之前意述,Java程序只能靠synchronized實現(xiàn)鎖的功能,在JavaSE5之后吮蛹,有了Lock接口荤崇。雖然缺少了synchronized的隱式獲取和釋放鎖的方便,但是擁有了鎖獲取與釋放的可操作性潮针、可中斷的獲取所以及超時獲取鎖等synchronized不具備的同步特性术荤。

????Lock接口提供的synchronized關鍵字不具備的主要特性:

? ? Lock是一個接口,它定義了鎖獲取與釋放的基本操作:

? ? void lock()? 每篷、void? lockInterruptibly() throws InterruptedException瓣戚、boolean? tryLock()?

????boolean? tryLock(long time,TimeUnit unit)throws InterruptedException、void unlock()

? ? Condition? newCondition()

5.2? 隊列同步器(AQS)

? ? ????隊列同步器AbstractQueuedSynchronizer,是用來構建鎖或其他同步組件的基礎框架焦读,它使用一個int類型的成員變量表示同步狀態(tài)子库,通過內置的FIFO隊列完成資源獲取線程的排隊工作。

? ? ? ? 同步器的主要使用方式是繼承矗晃,子類繼承同步同步器并實現(xiàn)它的抽象方法來管理同步狀態(tài)仑嗅,可以通過同步器提供的如下3個方法進行訪問和修改同步器狀態(tài):

? ? ? ? ?getState():獲取當前同步狀態(tài)

? ? ? ? ?setState(int newState):設置當前同步狀態(tài)

? ? ? ? ?compareAndSetState(int expect, int update):使用CAS設置當前狀態(tài),該方法能夠保證狀態(tài)設置的原子性。

? ? ? ? 以上3種方法能夠保證狀態(tài)改變的線程安全仓技,同步器支持獨占式和共享式獲取同步狀態(tài)鸵贬。

? ? 5.2.1? 隊列同步器的接口與示例

? ? ? ? 同步器的設計是基于模板方法模式的,也就是說使用者需要繼承同步器并重寫指定的方法脖捻,隨后將同步器組合在自定義同步組件的實現(xiàn)中阔逼,而這些模板方法將會調用使用者重寫的方法。

? ? ? ? 重寫同步器指定方法時地沮,使用以下3種方法訪問和修改同步狀態(tài):

? ? ? ? ? 1嗜浮、getState():獲取當前同步狀態(tài)

? ? ? ? ? 2、setState(int newState):設置當前同步狀態(tài)

? ? ? ? ? 3诉濒、compareAndSetState(int expect, int update):使用CAS設置當前狀態(tài)周伦,該方法能夠保證狀態(tài)設置的原子性。

? ?????同步器可以重寫的方法

同步器可以重寫的方法

? ? 實現(xiàn)自定義同步組件時未荒,將會調用同步器提供的模板方法专挪,同步器提供的模板方法如下:

同步器提供的模板方法

? ? 5.2.2? 隊列同步器的實現(xiàn)分析

? ? ????接下來從實現(xiàn)角度分析同步器是如何完成線程同步的,主要包括:同步隊列片排、獨占式同步狀態(tài)獲取與釋放寨腔、共享式同步狀態(tài)獲取與釋放以及超時獲取同步狀態(tài)等同步器核心數(shù)據(jù)結構與模板方法。

????1率寡、同步隊列

? ??????同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態(tài)的管理,當前線程獲取同步狀態(tài)失敗時,同步器會將當前線程以及等待狀態(tài)等信息構造成為一個節(jié)點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態(tài)釋放時,會把首節(jié)點中的線程喚醒,使其再次嘗試獲取同步狀態(tài)迫卢。

????????同步隊列中的節(jié)點(Node)用來保存獲取同步狀態(tài)失敗的線程引用、等待狀態(tài)以及前驅和后繼節(jié)點冶共。節(jié)點的屬性類型與名稱以及描述如表所示:

節(jié)點的屬性類型與名稱以及描述

? ??????節(jié)點是構成同步隊列(等待隊列,在5.6節(jié)中將會介紹)的基礎,同步器擁有首節(jié)點(head)和尾節(jié)點(tail),沒有成功獲取同步狀態(tài)的線程將會成為節(jié)點加入該隊列的尾部,同步隊列的基本結構如圖所示乾蛤。

同步隊列的基本結構

????????在圖中,同步器包含了兩個節(jié)點類型的引用,一個指向頭節(jié)點,而另一個指向尾節(jié)點。同步器提供了一個基于CAS的設置尾節(jié)點的方法:compareAndSetTail(Node expect,Nodeupdate),它需要傳遞當前線程“認為”的尾節(jié)點和當前節(jié)點,只有設置成功后,當前節(jié)點才正式與之前的尾節(jié)點建立關聯(lián)捅僵。

????????同步隊列遵循FIFO,首節(jié)點是獲取同步狀態(tài)成功的節(jié)點,首節(jié)點的線程在釋放同步狀態(tài)時,將會喚醒后繼節(jié)點,而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設置為首節(jié)點家卖。設置首節(jié)點是通過獲取同步狀態(tài)成功的線程來完成的,由于只有一個線程能夠成功獲取到同步狀態(tài),因此設置頭節(jié)點的方法并不需要使用CAS來保證,它只需要將首節(jié)點設置成為原首節(jié)點的后繼節(jié)點并斷開原首節(jié)點的next引用即可。

????2庙楚、獨占式同步狀態(tài)獲取與釋放

????????通過調用同步器的acquire(int arg)方法可以獲取同步狀態(tài),該方法對中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進入同步隊列中,后續(xù)對線程進行中斷操作時,線程不會從同步隊列中移出,該方法代碼:

同步器的acquire方法

? ??????上述代碼主要完成了同步狀態(tài)獲取上荡、節(jié)點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用自定義同步器實現(xiàn)的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態(tài),如果同步狀態(tài)獲取失敗,則構造同步節(jié)點(獨占式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態(tài))并通過addWaiter(Node node)方法將該節(jié)點加入到同步隊列的尾部,最后調用acquireQueued(Node node,int arg)方法,使得該節(jié)點以“死循環(huán)”的方式獲取同步狀態(tài)馒闷。如果獲取不到則阻塞節(jié)點中的線程,而被阻塞線程的喚醒主要依靠前驅節(jié)點的出隊或阻塞線程被中斷來實現(xiàn)酪捡。

? ??????下面分析一下相關工作,首先是節(jié)點的構造以及加入同步隊列:

同步器的addWaiter和enq方法

? ??????上述代碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節(jié)點能夠被線程安全添加纳账。在enq(final Node node)方法中,同步器通過“死循環(huán)”來保證節(jié)點的正確添加,在“死循環(huán)”中只有通過CAS將節(jié)點設置成為尾節(jié)點之后,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置逛薇。可以看出,enq(final Node node)方法將并發(fā)添加節(jié)點的請求通過CAS變得“串行化”了疏虫。

????????節(jié)點進入同步隊列之后,就進入了一個自旋的過程,每個節(jié)點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態(tài),就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節(jié)點的線程):

同步器的acquireQueued方法

????????在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環(huán)”中嘗試獲取同步狀態(tài),而只有前驅節(jié)點是頭節(jié)點才能夠嘗試獲取同步狀態(tài),這是為什么?原因有兩個:

????????第一,頭節(jié)點是成功獲取到同步狀態(tài)的節(jié)點,而頭節(jié)點的線程釋放了同步狀態(tài)之后,將會喚醒其后繼節(jié)點,后繼節(jié)點的線程被喚醒后需要檢查自己的前驅節(jié)點是否是頭節(jié)點金刁。

????????第二,維護同步隊列的FIFO原則帅涂。該方法中,節(jié)點自旋獲取同步狀態(tài)的行為如圖

節(jié)點自旋獲取同步狀態(tài)

????????由于非首節(jié)點線程前驅節(jié)點出隊或者被中斷而從等待狀態(tài)返回,隨后檢查自己的前驅是否是頭節(jié)點:如果是,則嘗試獲取同步狀態(tài)尤蛮∠庇眩可以看到節(jié)點和節(jié)點之間在循環(huán)檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否為頭節(jié)點,這樣就使得節(jié)點的釋放規(guī)則符合FIFO,并且也便于對過早通知的處理(過早通知是指前驅節(jié)點不是頭節(jié)點的線程由于中斷而被喚醒)。

????????獨占式同步狀態(tài)獲取流程,也就是acquire(int arg)方法調用流程:

??獨占式同步狀態(tài)獲取流程

????????前驅節(jié)點為頭節(jié)點且能夠獲取同步狀態(tài)的判斷條件和線程進入等待狀態(tài)是獲取同步狀態(tài)的自旋過程产捞。當同步狀態(tài)獲取成功之后,當前線程從acquire(int arg)方法返回,如果對于鎖這種并發(fā)組件而言,代表著當前線程獲取了鎖醇锚。

????????當前線程獲取同步狀態(tài)并執(zhí)行了相應邏輯之后,就需要釋放同步狀態(tài),使得后續(xù)節(jié)點能夠繼續(xù)獲取同步狀態(tài)。通過調用同步器的release(int arg)方法可以釋放同步狀態(tài),該方法在釋放了同步狀態(tài)之后,會喚醒其后繼節(jié)點(進而使后繼節(jié)點重新嘗試獲取同步狀態(tài))

同步器的release方法

????????該方法執(zhí)行時,會喚醒頭節(jié)點的后繼節(jié)點線程,unparkSuccessor(Node node)方法使用LockSupport(在后面的章節(jié)會專門介紹)來喚醒處于等待狀態(tài)的線程坯临。

????????分析了獨占式同步狀態(tài)獲取和釋放過程后,適當做個總結:在獲取同步狀態(tài)時,同步器維護一個同步隊列,獲取狀態(tài)失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節(jié)點為頭節(jié)點且成功獲取了同步狀態(tài)焊唬。在釋放同步狀態(tài)時,同步器調用tryRelease(int arg)方法釋放同步狀態(tài),然后喚醒頭節(jié)點的后繼節(jié)點。

????3看靠、共享式同步狀態(tài)獲取與釋放

? ???????通過調用同步器的acquireShared(int arg)方法可以獲取同步狀態(tài)

? ? ????在acquireShared(int arg)中赶促,同步器代用了tryAcquireShared(int arg)嘗試獲取同步狀態(tài),當其返回值大于等于0時挟炬,表示能夠獲取同步狀態(tài)鸥滨。因此,在共享獲取的自旋過程中谤祖,成功獲取到同步狀態(tài)并退出自旋的條件就是tryAcquireShared(int arg)的返回值大于等于0.在doAcquireShared(int arg)的自旋過程中婿滓,如果當前節(jié)點的前驅為頭結點時,嘗試獲取同步狀態(tài)粥喜,如果返回值大于等于0凸主,表示該次獲取同步狀態(tài)成功并且從自旋過程中退出。

? ? ? ? 與獨占式一樣额湘。共享式獲取也需要釋放同步狀態(tài)卿吐,通過調用releaseShared(int arg)可以釋放同步狀態(tài)。

? ? ? ? releaseShared(int arg)釋放同步狀態(tài)之后锋华,將會喚醒后續(xù)處于等待狀態(tài)的節(jié)點嗡官。對于能夠支持多個線程同時訪問的并發(fā)組件(如Semaphore)闲延,它和獨占式的主要區(qū)別在于tryReleaseShared(int arg)方法必須確保同步狀態(tài)線程安全釋放谜酒,一般通過循環(huán)和CAS來保證的,因為釋放同步狀態(tài)的操作會同時來自多個線程。

????4.獨占式超時獲取同步狀態(tài)

? ??????通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態(tài),即在指定的時間段內獲取同步狀態(tài),如果獲取到同步狀態(tài)則返回true,否則,返回false芥丧。該方法提供了傳統(tǒng)Java同步操作(比如synchronized關鍵字)所不具備的特性。

????????在分析該方法的實現(xiàn)前,先介紹一下響應中斷的同步狀態(tài)獲取過程坊罢。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標志位會被修改,但線程依舊會阻塞在synchronized上,等待著獲取鎖续担。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態(tài)時,如果當前線程被中斷,會立刻返回,并拋出InterruptedException。

????????超時獲取同步狀態(tài)過程可以被視作響應中斷獲取同步狀態(tài)過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性活孩。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大于0則表示超時時間未到,需要繼續(xù)睡眠nanosTimeout納秒,反之,表示已經(jīng)超時物遇。

????????該方法在自旋過程中,當節(jié)點的前驅節(jié)點為頭節(jié)點時嘗試獲取同步狀態(tài),如果獲取成功則從該方法返回,這個過程和獨占式同步獲取的過程類似,但是在同步狀態(tài)獲取失敗的處理上有所不同。如果當前線程獲取同步狀態(tài)失敗,則判斷是否超時(nanosTimeout小于等于0表示已經(jīng)超時),如果沒有超時,重新計算超時間隔nanosTimeout,然后使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object?blocker,long nanos)方法返回)。

????????如果nanosTimeout小于等于spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程询兴。原因在于,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現(xiàn)得反而不精確乃沙。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。

????????獨占式超時獲取同步狀態(tài)doAcquireNanos(int arg,long nanosTimeout)和獨占式獲取同步狀態(tài)acquire(int args)在流程上非常相似,其主要區(qū)別在于未獲取到同步狀態(tài)時的處理邏輯诗舰。acquire(int args)在未獲取到同步狀態(tài)時,將會使當前線程一直處于等待狀態(tài),而doAcquireNanos(int arg,long nanosTimeout)會使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內沒有獲取到同步狀態(tài),將會從等待邏輯中自動返回警儒。

? ? 5、自定義同步組件——TwinsLock

5.3? 重入鎖

? ? ?重入鎖ReentrantLock,能夠支持一個線程對資源的重復加鎖眶根,除此之外蜀铲,該鎖還支持獲取鎖時的公平與非公平選擇。

? ? 1属百、實現(xiàn)重進入

? ????重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖所阻塞记劝,該特性的實現(xiàn)需要解決以下兩個問題。

? ? ? 1)線程再次獲取鎖族扰。鎖需要去識別獲取鎖的線程是否為當前占據(jù)鎖的線程厌丑,如果是,則再次成功獲取别伏。

? ? ? 2)鎖的最終釋放蹄衷。線程重復n次獲取了鎖,隨后在第n次釋放該鎖后厘肮,其他線程能夠獲取到該鎖愧口,鎖的最終釋放要求鎖對于獲取進行計數(shù)自增,計數(shù)表示當前鎖被重復獲取的次數(shù)类茂,而鎖被釋放時耍属,計數(shù)自減,當計數(shù)等于0時表示鎖已經(jīng)成功釋放巩检。

? ??ReentrantLock是通過組合自定義同步器來實現(xiàn)鎖的獲取與釋放厚骗,以非公平行(默認的)實現(xiàn)為例:

ReentrantLock的nonfairTryAcquire方法

? ??????該方法增加了再次獲取同步狀態(tài)的處理邏輯:通過判斷當前線程是否為獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求兢哭,則將同步狀態(tài)值進行增加并返回true领舰,表示獲取同步狀態(tài)成功。

ReentrantLock的tryRelease方法

? ??????如果該鎖被獲取了n次迟螺,那么前n-1次tryRelease(int releases)方法必須返回false冲秽,而只有同步狀態(tài)安全釋放了,才能返回true矩父★鄙#可以看到,該方法將同步狀態(tài)是否為0作為最終釋放的條件窍株,當同步狀態(tài)為0時民轴,將占有線程設置為null攻柠,并返回true,表示釋放成功后裸。

????2瑰钮、公平與非公平獲取鎖的區(qū)別

? ????公平性與否是針對獲取鎖而言的,如果一個鎖是公平的微驶,那么鎖的獲取順序就應該符合請求的絕對時間順序飞涂,也就是FIFO。

ReentrantLock的tryAcquire方法

????????該方法與nofairTryAcquire(int acquires)比較祈搜,唯一不同的位置為判斷條件多了hasQueuedPredecessors()方法较店,即加入同步隊列中當前節(jié)點是否有前驅節(jié)點的判斷,如果該方法返回true容燕,表示有線程比當前線程更早地請求獲取鎖梁呈,因為需要等待前驅線程獲取并釋放鎖之后才能繼續(xù)獲取鎖。

????????? 公平性鎖保證了鎖的獲取按照FIFO原則蘸秘,而代價是進行大量的線程切換官卡。非公平性鎖雖然可能造成線程”饑餓”,但極少的線程切換醋虏,保證了其更大的吞吐量寻咒。

5.4? 讀寫鎖

????????讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時颈嚼,所有的讀線程和其他寫線程均被阻塞毛秘。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖阻课,通過分離讀鎖和寫鎖叫挟,使得并發(fā)性相比一般的排他鎖有了很大提升。

? ? ? ? 一般情況下限煞,讀寫鎖性能都會比排它鎖好抹恳,適用于讀多于寫的情況。Java并發(fā)包提供的讀寫鎖的實現(xiàn)是ReentrantReadWriteLock署驻,提供的特性如下:公平性選擇奋献、重入性、鎖降級旺上。

????5.4.1? 讀寫鎖的接口與示例

? ? ReadWriteLock僅定義了獲取讀鎖和寫鎖這兩個方法瓶蚂,即readLock()和writeLock()方法,而其實現(xiàn)類——ReentrantReadWriteLock抚官,處理接口方法之外扬跋,還提供了一些便于外界監(jiān)控其內部工作狀態(tài)的方法阶捆,如下:

ReentrantReadWriteLock展示內部工作狀態(tài)的方法

? ? 5.4.2? 讀寫鎖的實現(xiàn)分析

????接下來分析ReentrantReadWriteLock的實現(xiàn)凌节,主要包括:讀寫狀態(tài)設計钦听、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級

? ? 1倍奢、讀寫狀態(tài)設計

? ??????讀寫鎖同樣依賴自定義同步器來實現(xiàn)同步功能朴上,而讀寫狀態(tài)就是其同步器的同步狀態(tài)∽渖罚回想ReentrantLock中自定義同步器的實現(xiàn)痪宰,同步狀態(tài)表示鎖被一個線程重復獲取的次數(shù),而讀寫鎖的自定義同步器需要在同步狀態(tài)(一個整數(shù)變量)上維護多個讀線程和一個寫線程的狀態(tài)畔裕,使得該狀態(tài)的設計成為讀寫鎖實現(xiàn)的關鍵衣撬。

? ????????如果在一個整數(shù)變量上維護多種狀態(tài),就一定需要”按位切割使用“這個變量扮饶,讀寫鎖將變量切分了兩個部分具练,高16位表示讀,低16位表示寫甜无。

讀寫鎖狀態(tài)劃分的方式

?? ? ????上圖中的同步狀態(tài)表示一個線程已經(jīng)獲取了寫鎖扛点,并且重進入了2次;同時也連續(xù)獲取了2次讀鎖岂丘。讀寫鎖是如何迅速確定讀和寫各自的狀態(tài)呢陵究?答案是:通過位運算。假設當前同步狀態(tài)值為S奥帘,寫狀態(tài)等于S&0x0000FFFF(將高16位全部抹去)铜邮,讀狀態(tài)等于S>>>16(無符號補0右移16位)。當寫狀態(tài)增加1時寨蹋,等于S+1牲距,當讀狀態(tài)增加1時,等于S+(1<<16)钥庇,也就是S+0x00010000牍鞠。

????????根據(jù)狀態(tài)的劃分能得出一個推論:S不等于0時,當寫狀態(tài)(S&0x0000FFFF)等于0時评姨,則讀狀態(tài)(S>>>16)大于0难述,即讀鎖已被獲取。

????2吐句、寫鎖的獲取與釋放

?????????寫鎖是一個支持重進入的排他鎖胁后。如果當前線程已經(jīng)獲取了寫鎖,則增加寫狀態(tài)嗦枢。如果當前線程在獲取寫鎖時攀芯,讀鎖已經(jīng)被獲取(讀狀態(tài)不為0)或者該線程已經(jīng)獲取寫鎖的線程,則當前線程進入等待狀態(tài)文虏。

ReentrantReadWriteLock的tryAcquire方法

????????該方法除了重入條件(當前線程為獲取了寫鎖的線程)之外侣诺,增加了一個讀鎖是否存在的判斷殖演。如果存在讀鎖,則寫鎖不能被獲取年鸳,原因在于:讀寫鎖要確保寫鎖的操作對讀鎖可見趴久,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當前寫線程的操作搔确。因此彼棍,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取膳算,而寫鎖一旦被獲取座硕,則其他讀寫線程的后續(xù)訪問均被阻塞。

?? ? ? ? 寫鎖的釋放與ReentrantLock的釋放過程基本類似涕蜂,每次釋放均減少寫狀態(tài)坎吻,當寫狀態(tài)為0時,表示寫鎖已被釋放宇葱,從而等待的讀寫線程能夠繼續(xù)訪問讀寫鎖瘦真,同時前次寫線程的修改對后續(xù)讀寫線程可見。

?????3黍瞧、讀鎖的獲取與釋放

????????讀鎖是一個支持重進入的共享鎖诸尽,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態(tài)為0)時印颤,讀鎖總會被成功地獲取您机,而所做的也只是(線程安全的)增加讀狀態(tài)。如果當前線程已經(jīng)獲取了讀鎖年局,則增加讀狀態(tài)际看。如果當前線程在獲取讀鎖時,寫鎖已經(jīng)被其他線程獲取矢否,則進入等待狀態(tài)仲闽。

ReentrantReadWriteLock的tryAcquireShared方法

????????在tryAcquireShared(int unused)方法中,如果其他線程已經(jīng)獲取了寫鎖僵朗,則當前線程獲取讀鎖失敗赖欣,進入等待狀態(tài)。如果當前線程獲取了寫鎖或者寫鎖未被獲取验庙,則當前線程(線程安全顶吮,依靠CAS保證)增加讀狀態(tài),成功獲取讀鎖粪薛。讀鎖的每次釋放均減少讀狀態(tài)悴了,減少的值是(1<<16)。

????4、鎖降級

????????鎖降級:指的是寫鎖降級成為讀鎖湃交。如果當前線程擁有寫鎖熟空,然后將其釋放,最后再獲取讀鎖巡揍,這種分段完成的過程不能稱之為鎖降級,鎖降級是指把持住(當前擁有的)寫鎖菌瘪,再獲取到讀鎖腮敌,隨后釋放(先前擁有的)寫鎖的過程。

????????鎖降級中讀鎖的獲取是必要的俏扩。主要是為了保證數(shù)據(jù)的可見性糜工,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程獲取了寫鎖并修改了數(shù)據(jù)录淡,那么當前線程無法感知線程T的數(shù)據(jù)更新捌木。如果當前線程獲取讀鎖,即遵循鎖降級的步驟嫉戚,則線程T將被阻塞刨裆,直到當前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進行數(shù)據(jù)更新彬檀。

?5.5? LockSupport工具

????????LockSupport定義了一組公共的靜態(tài)方法帆啃,這些方法提供了最基本的阻塞或者喚醒的功能,也是構建同步組件的基礎工具窍帝。

LockSupport提供的阻塞和喚醒方法

5.6? Condition接口

? ? ????任意一個Java對象都擁有一組監(jiān)視器方法(定義在java.lang.Object上)对嚼,主要包括:wait()呻此、wait(long? timeout)、notify()、notifyAll(),這些方法和synchronized關鍵字結合使用度液,可以實現(xiàn)等待/通知模式。

????????Condition接口也提供了類似Object的監(jiān)視器方法剩辟,與Lock接口配合使用實現(xiàn)等待/通知模式池充。對比Object的監(jiān)視器方法和Condition接口,可以更加詳細的了解Condition 的特性:

? ? 5.6.1? Condition接口與示例

? ??????? Condition定義了等待/通知兩種類型的方法飞苇,當前線程調用這些方法時刑峡,需要提前獲取到Condition對象關聯(lián)的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創(chuàng)建出來的玄柠,換句話說突梦,Condition是依賴Lock對象的。

? ? ? ? Condition定義的(部分)方法以及描述:

Condition定義的(部分)方法以及描述

????5.6.2 Condition的實現(xiàn)分析

????????ConditionObject是同步器AbstractQueuedSynchronizer的內部類羽利,因為Condition的操作需要獲取相關聯(lián)的鎖宫患,所以作為同步器的內部類也較為合理。 關于Condition的實現(xiàn)这弧,主要包括:等待隊列娃闲、等待和通知虚汛。

????1、等待隊列?

????等待隊列是一個FIFO的隊列皇帮,在隊列中的每個節(jié)點都包含了一個線程引用卷哩,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法属拾,那么該線程將會釋放鎖将谊、構造成節(jié)點加入到等待隊列并進入等待狀態(tài)。事實上渐白,同步隊列和等待隊列中的節(jié)點類型都是同步器的靜態(tài)內部類AbstractQueuedSynchronizer.Node尊浓。

? ? 一個Condition包含一個等待隊列,Condition有首節(jié)點和尾節(jié)點纯衍。當前線程調用Condition.await()時會將當前線程構造節(jié)點栋齿,并將節(jié)點從尾部加入到等待隊列〗笾睿基本結構如下:?

? ? ? ? 將新增節(jié)點添加到等待隊列尾部不需要CAS操作瓦堵,因為調用await()方法的線程肯定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的歌亲。

? ??????在Object的監(jiān)視器模型上谷丸,一個對象擁有一個同步隊列和一個等待隊列;而并發(fā)包中的Lock擁有一個同步隊列和多個等待隊列应结,其對應關系是:?

????2刨疼、等待

????????調用Condition的await(),或以await開頭的方法鹅龄,會使當前線程進入等待隊列并釋放鎖揩慕,同時線程狀態(tài)變?yōu)榈却隣顟B(tài)。當從await()方法中返回時扮休,一定是獲取了Condition相關聯(lián)的鎖迎卤。?

????????如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時玷坠,相當于同步隊列的首節(jié)點(獲取了鎖的節(jié)點)移動到了等待隊列中蜗搔。?

????????Condition的await()方法源碼如下:

? ? ? ? 調用了該方法的線程成功獲取了鎖的線程,也就是同步隊列中的首節(jié)點八堡,該方法會將當前線程構造出節(jié)點并加入等待隊列中樟凄,然后釋放同步狀態(tài),喚醒同步隊列中的后繼結點兄渺,然后當前線程會進入等待狀態(tài)缝龄。

? ? ? ? 當?shù)却犃兄械慕Y點被喚醒,則喚醒結點的線程開始嘗試獲取同步狀態(tài)。如果不是通過其他線程調用的signal()喚醒叔壤,而是對等待線程進行中斷瞎饲,則會拋出InterruptedException.

????當前線程加入到等待隊列的過程:

????3、通知

? ??????調用Condition的signal()炼绘,將會喚醒在等待隊列中等待時間最長的節(jié)點(首節(jié)點)嗅战,在喚醒節(jié)點之前,會將節(jié)點移到同步隊列中俺亮。 Condition的signal()方法源碼:

????????調用該方法的前置條件是當前線程必須獲取鎖驮捍,接著獲取等待隊列的首節(jié)點,將其移動到同步隊列并使用LockSupport喚醒節(jié)點中的線程铅辞。

????????節(jié)點從等待隊列移到同步隊列的過程:

????????Condition的signalAll()方法厌漂,相當于等待隊列中的每個節(jié)點均被執(zhí)行一次signal()方法萨醒,效果就是將等待隊列中所有節(jié)點全部移動到同步隊列中斟珊,并喚醒每個節(jié)點的線程。

5.7? 本章小結


第6章? Java并發(fā)容器和框架

6.1? ?ConcurrentHashMap的實現(xiàn)原理與使用

? ??????ConcurrentHashMap是線程安全且高效的HashMap富纸。

? ? 6.1.1? 為什么要使用ConcurrentHashMap

? ? ? ? 在并發(fā)編程中使用HashMap可能導致程序死循環(huán)囤踩,而使用線程安全的HashTable效率又非常低,基于以上2個原因晓褪,便有了ConcurrentHashMap的出現(xiàn)堵漱。

? ? (1)線程不安全的HashMap

? ? ? ? 再多線程環(huán)境下,使用HashMap進行put操作時會引起死循環(huán)涣仿,導致CPU利用率接近100%勤庐,原因是多線程會導致HashMap的Entry鏈表形成環(huán)形數(shù)據(jù)結構,一旦形成環(huán)形數(shù)據(jù)結構好港,Entry的next節(jié)點永遠不為空愉镰,就會產(chǎn)生死循環(huán)獲取Entry。

????(2)效率低下的HashTable

????????HashTable容器使用synchronized來保證線程安全钧汹,但在線程競爭激烈的情況下HashTable的效率非常低下丈探。因為當一個線程訪問HashTable的同步方法,其他線程也訪問HashTable的同步方法時拔莱,會進入阻塞或輪詢狀態(tài)碗降。

????(3)ConcurrentHashMap的鎖分段技術可優(yōu)先提升并發(fā)訪問效率

????????首先將數(shù)據(jù)分成一段一段地存儲,然后給每一段數(shù)據(jù)配一把鎖塘秦,當一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候讼渊,其他段的數(shù)據(jù)也能被其他線程訪問。

????6.1.2??ConcurrentHashMap的結構

? ??????ConcurrentHashMap是由Segment數(shù)組結構和HashEntry數(shù)組結構組成尊剔。Segment是一種可重入鎖(ReentrantLock)精偿,在ConcurrentHashMap里扮演鎖的角色;HashEntry則用于存儲鍵值對數(shù)據(jù)。一個ConcurrentHashMap里包含一個Segment數(shù)組笔咽。Segment的結構和HashMap類似搔预,是一種數(shù)組和鏈表結構。一個Segment里包含一個HashEntry數(shù)組叶组,每個HashEntry是一個鏈表結構的元素拯田,每個Segment守護著一個HashEntry數(shù)組里的元素,當對HashEntry數(shù)組的數(shù)據(jù)進行修改時甩十,必須首先獲得與它對應的Segment鎖船庇。

6.1.3??ConcurrentHashMap的初始化

? ??????ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor侣监、concurrencyLevel幾個參數(shù)來初始化segments數(shù)組鸭轮、段偏移量segmentShift,段掩碼segmentMask和每個segment里的HashEntry數(shù)組 橄霉。

? ? ? 1窃爷、初始化segments數(shù)組

????????由上面的代碼可知segments數(shù)組的長度ssize通過concurrencyLevel計算得出。為了能通過按位與的哈希算法來定位segments數(shù)組的索引姓蜂,必須保證segments數(shù)組的長度是2的N次方(power-of-two size)按厘,所以必須計算出一個是大于或等于concurrencyLevel的最小的2的N次方值來作為segments數(shù)組的長度。假如concurrencyLevel等于14钱慢,15或16逮京,ssize都會等于16,即容器里鎖的個數(shù)也是16束莫。?

????2懒棉、初始化segmentShift和segmentMask:?

????????這兩個全局變量在定位segment時的哈希算法里需要使用,sshift等于ssize從1向左移位的次數(shù)览绿,在默認情況下concurrencyLevel等于16策严,1需要向左移位移動4次,所以sshift等于4挟裂。segmentShift用于定位參與hash運算的位數(shù)享钞,segmentShift等于32減sshift,所以等于28诀蓉,這里之所以用32是因為ConcurrentHashMap里的hash()方法輸出的最大數(shù)是32位的栗竖。segmentMask是哈希運算的掩碼,等于ssize減1渠啤,即15狐肢,掩碼的二進制各個位的值都是1。因為ssize的最大長度是65536沥曹,所以segmentShift最大值是16份名,segmentMask最大值是65535碟联,對應的二進制是16位,每個位都是1僵腺。?

????3鲤孵、初始化每個segment:?

????????輸入?yún)?shù)initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每個segment的負載因子辰如,在構造方法里需要通過這兩個參數(shù)來初始化數(shù)組中的每個segment普监。?

????????上面代碼中的變量cap就是segment里HashEntry數(shù)組的長度,它等于initialCapacity除以ssize的倍數(shù)c琉兜,如果c大于1凯正,就會取大于等于c的2的N次方值,所以cap不是1豌蟋,就是2的N次方廊散。segment的容量threshold=(int)cap*loadFactor,默認情況下initialCapacity等于16梧疲,loadfactor等于0.75允睹,通過運算cap等于1,threshold等于零往声。?

【備注】:參數(shù)concurrencyLevel是用戶估計的并發(fā)級別擂找,就是說你覺得最多有多少線程共同修改這個map戳吝,根據(jù)這個來確定Segment數(shù)組的大小浩销,默認為16。?

????6.1.4? 定位Segment?

????????既然ConcurrentHashMap使用分段鎖Segment來保護不同段的數(shù)據(jù)听哭,那么在插入和獲取元素的時候慢洋,必須先通過哈希算法定位到Segment÷脚蹋可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種算法對元素的hashCode進行一次再哈希普筹。

????????之所以進行再哈希,其目的是為了減少哈希沖突隘马,使元素能夠均勻的分布在不同的Segment上太防,從而提高容器的存取效率。假如哈希的質量差到極點酸员,那么所有的元素都在一個Segment中蜒车,不僅存取元素緩慢,分段鎖也會失去意義幔嗦。

????6.1.5??ConcurrentHashMap的操作?

? ? ????本節(jié)介紹ConcurrentHashMap的3種操作——get酿愧、put、size

? ? ? 1邀泉、get操作

? ? ? ? Segment的get操作實現(xiàn)非常簡單和高效嬉挡。先經(jīng)過一次再散列钝鸽,然后使用這個散列值通過散列運算定位到Segment,再通過散列算法定位到元素。?

????????get操作的高效之處在于整個get過程不需要加鎖庞钢,除非讀到值為空才會加鎖重讀拔恰。因為用于統(tǒng)計當前Segment大小的count字段和用于存儲值得HashEntry的value都被定義成volatile變量,而在get操作里只需要讀不需要寫共享變量count和value。在定位元素的代碼里我們可以發(fā)現(xiàn)基括,定位HashEntry和定位Segment的散列算法雖然一樣仁连,都與數(shù)組的長度減去1再相“與”,但是相“與”的值不一樣,定位Segment使用的是元素的hashcode通過再散列后得到的值得高位阱穗,而定位HashEntry直接使用的是再散列后的值饭冬。其目的是避免兩次散列后的值一樣,雖然元素在Segment里散列開了揪阶,但是卻沒有在HashEntry里散列開昌抠。?

????2、put操作

????????由于put方法里需要對共享變量進行寫入操作鲁僚,所以為了線程安全炊苫,在操作共享變量時必須加鎖。put方法首先定位到Segment,然后再Segment里進行插入操作冰沙。插入操作需要經(jīng)歷兩個步驟侨艾,第一步在插入元素之前判斷Segment里的HashEntry數(shù)組是否需要擴容,如果HashEntry數(shù)組超過容量拓挥,則創(chuàng)建一個容量是原來容量兩倍的數(shù)組唠梨,然后將原數(shù)組里的元素進行再散列后插入到新的數(shù)組里。為了高效侥啤,ConcurrentHashMap只針對某個Segment進行擴容而不是整個容器当叭。第二步定位添加元素的位置码俩,然后將其放在HashEntry數(shù)組里挑宠。?

????3、size操作?

????????如果要統(tǒng)計整個ConcurrentHashMap里元素的大小有梆,就必須統(tǒng)計所有Segment里元素的大小后求和赁炎。Segment里的全局變量count雖然被定義為volatile變量醉箕,但如果在累加前使用的count發(fā)生了變化,那么統(tǒng)計結果就不準了徙垫。最安全的做法是在統(tǒng)計size時讥裤,鎖住所有的Segment的put,remove,clean方法,但顯然很低效松邪。?

????????ConcurrentHashMap統(tǒng)計size的方法是坞琴,嘗試2次不鎖住Segment的方式來統(tǒng)計各個Segment大小,如果在統(tǒng)計過程中逗抑,容器的count發(fā)生了變化剧辐,則再采用加鎖的方式來統(tǒng)計所有Segment的大小寒亥。ConcurrentHashMap中modCount變量在調用put,remove和clean方法素前加1,從而來記錄容器大小是否發(fā)生變化荧关。

6.2? ConcurrentLinkedQueue

? ??????在并發(fā)編程中溉奕,如果要實現(xiàn)一個線程安全的隊列,則有兩種方式:?

? ? ? ? (1) 使用阻塞算法忍啤,入隊和出隊使用鎖來控制加勤。?

? ? ? ? (2) 非阻塞算法:即循環(huán)CAS方式來實現(xiàn)。?

????????ConcurrentLinkedQueue就是使用非阻塞方式實現(xiàn)的基于鏈接節(jié)點無界線程安全隊列同波。它采用先進先出的規(guī)則對節(jié)點進行排序鳄梅。新添加的元素會被添加到對尾,獲取元素時未檩,它會返回頭部的元素戴尸。?

6.2.1??ConcurrentLinkedQueue的結構

????????ConcurrentLinkedQueue的類圖如下:?

? ??????ConcurrentLinkedQueue由head節(jié)點和tail節(jié)點組成,每個節(jié)點(Node)由節(jié)點元素(item)和指向下一個節(jié)點(next)的引用組成冤狡,由此組成一張鏈表結構的隊列孙蒙。默認情況下head節(jié)點存儲的元素為空,tail節(jié)點等于head節(jié)點悲雳。?

? ??6.2.2 入隊列?

? ? ? 1挎峦、入隊列的過程

????????入隊列就是將入隊節(jié)點添加到隊列的尾部。?

????????入隊主要做兩件事情:

? ? ? ? 1合瓢、入隊節(jié)點設置為當前隊列尾節(jié)點的下一個節(jié)點坦胶。?

? ? ? ? 2、更新tail節(jié)點歪玲,如果tail節(jié)點的next節(jié)點不為空迁央,則將入隊節(jié)點設置為tail節(jié)點掷匠,如果tail節(jié)點的next為空滥崩,則將入隊節(jié)點設置為tail節(jié)點的next節(jié)點(注意:此時并未更新tail節(jié)點為尾節(jié)點)。所以讹语,tail節(jié)點并不總是尾節(jié)點钙皮。?

????????如果在單線程中執(zhí)行沒有任何問題,但如果在多線程中可能出現(xiàn)插隊的情況顽决。如果有一個線程正在入隊短条,那么首先獲取尾節(jié)點,然后設置尾節(jié)點的下一個節(jié)點為入隊節(jié)點才菠,但這是如果另一個線程插隊茸时,則隊列尾節(jié)點發(fā)生變化,當前線程需要暫停入隊操作赋访,重新獲取新的尾節(jié)點可都。所以使用CAS算法來將入隊節(jié)點設置為尾節(jié)點的next節(jié)點:?


? ??2缓待、定位尾節(jié)點?

????????tail節(jié)點并不總是尾結點,所以每次入隊都必須先通過tail節(jié)點來找到尾結點渠牲。尾節(jié)點可能是tail節(jié)點或tail節(jié)點的next節(jié)點旋炒。


????????如果隊列尾節(jié)點p與p的next節(jié)點都為空,則表示這個隊列剛初始化签杈,正準備添加節(jié)點瘫镇,所以需要返回head節(jié)點。?

? ??3答姥、設置入隊節(jié)點為尾節(jié)點?

????????p.casNext(null,n)方法用于將入隊節(jié)點設置為當前隊列尾節(jié)點的next節(jié)點铣除,如果p是null,表示p是當前隊列的尾節(jié)點,如果不為null鹦付,表示有其他線程更新了尾節(jié)點通孽,則需要重新獲取當前隊列的尾節(jié)點。?

????4睁壁、HOPS的設計意圖

? ??????為什么不保證tail節(jié)點總是尾節(jié)點?背苦?

????????如果保證tail節(jié)點總是尾節(jié)點的話,那么入隊操作直接通過tail節(jié)點定位到尾節(jié)點,然后把尾節(jié)點next節(jié)點更新為新的入隊節(jié)點,隨后更新tail節(jié)點為新的尾節(jié)點不就可以了嗎?但這么做的有一個很明顯的缺陷:每次都需要使用循環(huán)CAS更新tail節(jié)點為尾節(jié)點。一定程度上降低了入隊的效率潘明。所以在ConcurrentLinkedQueue入隊時行剂,并不是每次更新tail節(jié)點為尾節(jié)點,只有當tail節(jié)點和尾節(jié)點距離大于等于常量HOPS的值(默認為1)時才會更新tail節(jié)點钳降。tail節(jié)點與尾節(jié)點距離越長厚宰,使用CAS更新tail節(jié)點次數(shù)越少,但每次入隊時通過tail節(jié)點定位尾節(jié)點的時間就越長遂填。但這樣仍然可以提升入隊效率铲觉,因為本質上來看通過增加volatile變量的讀操作來減少volatile變量的寫操作,而對volatile變量寫操作的開銷遠遠大于讀操作吓坚。?

? ??????【備注】:入隊方法永遠返回true撵幽,所以不要通過返回值判斷入隊是否成功。?

? ??6.2.3? 出隊列?

????????出隊列就是從隊列里返回一個節(jié)點元素礁击,并清空該節(jié)點對元素的引用盐杂。?

????????以下為從隊列獲取4個元素的快照圖:?

????????從上圖可以看出,并不是每次出隊列都需要更新head節(jié)點為首節(jié)點哆窿,當head節(jié)點為空時链烈,更新head節(jié)點為首節(jié)點,如果head節(jié)點不為空挚躯,則直接彈出head節(jié)點里的元素强衡,并不會更新head節(jié)點為新的首節(jié)點。之所以這樣設計码荔,同樣是為了減少CAS更新head節(jié)點從而提高出隊效率漩勤。?

????????首先獲取首節(jié)點号涯,然后判斷首節(jié)點是否為空,如果為空锯七,則證明另一個線程已經(jīng)進行了一次出隊操作链快,需要獲取新首節(jié)點即原首節(jié)點的next節(jié)點。如果不為空則使用CAS方式將首節(jié)點引用設置為null,如果成功眉尸,則直接返回首節(jié)點的元素域蜗,如果不成功,則表示另外一個線程已經(jīng)進行了一次出隊操作并更新了head節(jié)點噪猾,導致元素發(fā)生變化霉祸,需要重新獲取首節(jié)點。

6.3? Java中的阻塞隊列

? ??6.3.1? 什么是阻塞隊列袱蜡??

????阻塞隊列是一個支持兩個附加操作的隊列丝蹭。 這兩個附加操作支持阻塞的插入和移除方法。

? ? ? ? 1)支持阻塞的插入方法:當隊列滿時坪蚁,隊列會阻塞插入元素的線程奔穿,直到隊列不滿。?

? ? ? ? 2)支持阻塞的移除方法:在隊列為空時敏晤,獲取元素的線程會等待隊列變成非空贱田。?

? ? 阻塞隊列常用于生產(chǎn)者和消費者的場景,生產(chǎn)者向隊列里添加元素嘴脾,消費者從隊列里取元素男摧。在阻塞隊列不可用時(消費者取時,隊列為空译打,生產(chǎn)者添加時耗拓,隊列已滿),這兩個附加操作提供了4中處理方式:?

????- 拋出異常:當隊列滿時奏司,如果再往隊列里插入元素乔询,會拋出IllegalStateException(“Queuefull”)異常。當隊列空時结澄,從隊列獲取元素會拋出NoSuchElmentException異常哥谷。?

????- 返回特殊值:當往隊列插入元素時,會返回元素是否插入成功麻献,成功返回true。當從隊列取元素時猜扮,如果沒有則返回null勉吻。?

????- 一直阻塞:當隊列滿時,如果生產(chǎn)者線程往隊列put元素旅赢,則隊列會一直阻塞生產(chǎn)者線程直到隊列可用或響應中斷齿桃。當隊列空時惑惶,如果消費者線程從隊列里take元素,隊列會阻塞消費者線程直到隊列不為空短纵。?

????- 超時退出:當隊列滿時带污,如果生產(chǎn)者線程往隊列里插入元素,隊列會阻塞生產(chǎn)者線程一段時間香到,如果超過了指定的時間鱼冀,生產(chǎn)者線程就會退出。當隊列空時悠就,如果消費者線程從隊列取元素千绪,隊列會阻塞消費者線程一段時間,直到超過指定的時間梗脾,消費者線程退出荸型。?

? ??【備注】:如果是無界阻塞隊列,隊列不可能出現(xiàn)滿的情況炸茧,所以使用put或offer方法永遠不會被阻塞瑞妇,而且使用offer方法時,永遠返回true梭冠。?

6.3.2? Java里的阻塞隊列

? ??????JDK7 提供的7個阻塞隊列?:

? ? (1)ArrayBlockingQueue:一個由數(shù)組結構組成的有界阻塞隊列踪宠。?

????????此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證線程公平的訪問隊列妈嘹。但可通過以下代碼創(chuàng)建一個公平的阻塞隊列:?

????為了保證公平性柳琢,通常會降低吞吐量,其實現(xiàn)是依靠可重入鎖:?

? ??(2)LinkedBlockQueue:一個由鏈表結構組成的有界阻塞隊列润脸。?

????????此隊列的默認和最大長度為Integer.MAX_VALUE柬脸。按照先進先出(FIFO)的原則對元素進行排序。但不保證線程公平的訪問隊列毙驯,也不提供創(chuàng)建公平訪問隊列的方法倒堕。?

? ??(3)PriorityBlockingQueue:一個支持優(yōu)先級排序的無界阻塞隊列。?

????????默認情況下元素采用自然順序升序排序爆价。也可自定義類實現(xiàn)compareTo()方法來執(zhí)行元素排序規(guī)則垦巴,或初始化PriorityBlockingQueue時,執(zhí)行構造參數(shù)Comparator來對元素進行排序铭段。但PriorityBlockingQueue不能保證同優(yōu)先級元素的順序骤宣。?

? ??(4)DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列。?

????????DelayQueue是一個支持演示獲取元素的無界阻塞隊列序愚。隊列使用PriorityQueue實現(xiàn)憔披。隊列中的元素必須實現(xiàn)Delayed接口,在創(chuàng)建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素芬膝。?

????????DelayQueue可以用在以下場景:?

? ? ? ? 1)緩存系統(tǒng)的設計:用來保存緩存元素的有效期望门。使用一個線程循環(huán)查詢DelayQueue,一旦能從DelayQueue中獲取元素時锰霜,表示緩存有效期到了筹误。?

????????2)定時任務調度:使用DelayQueue保存當天將會執(zhí)行的任務和執(zhí)行時間,一旦從DelayQueued中獲取到任務就開始執(zhí)行癣缅,如TimerQueue就是使用DelayQueue實現(xiàn)的厨剪。

? ??(5)SynchronousQueue:一個不存儲元素的阻塞隊列。?

????????此隊列每一個put操作必須等待一個take操作所灸,否則不能繼續(xù)添加元素丽惶。默認情況下線程采用非公平性策略訪問隊列,但可通過以下方法設置以公平策略訪問:?

????????SynchronousQueue本身不存儲任何元素爬立,只是負責把生產(chǎn)者線程處理的數(shù)據(jù)直接傳遞給消費者線程钾唬。其吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。?

? ? (6)LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列侠驯。?

????????相比其他的阻塞隊列抡秆,LinkedTransferQueue多了tryTransfer和transfer方法。?

????????transfer方法:如果當前有消費者正在等待接收元素吟策,transfer方法可以把生產(chǎn)者傳入的元素立刻transfer給消費者儒士。如果沒有消費者等待接受元素,transfer方法會將元素存放在隊列的tail節(jié)點檩坚,直到該元素被消費者消費才返回着撩。?

????????tryTransfer方法:tryTransfer方法用來試探生產(chǎn)者傳入的元素是否能直接傳給消費者消費。如果沒有消費者等待接收元素匾委,返回false拖叙,反之,返回true赂乐。tryTransfer方法會立即返回薯鳍,而不用等待元素被消費以后才返回。?

????????對于帶有時間限制的tryTransfer(E e,long timeout,TimeUnit unit)方法挨措,試圖把生產(chǎn)者傳入的元素直接傳給消費者挖滤,但如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒有消費元素浅役,則返回false斩松,如果在超時時間內消費了元素則返回true。?

? ??(7)LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列担租。?

LinkedBlockingDeque是一個可以從隊列兩端插入和移除元素的隊列砸民。因為與其他阻塞隊列相比,多了一個操作隊列的入口滋早,所以在多線程同時入隊時掂僵,也就減少一半的競爭签夭。?

????6.3.3? 阻塞隊列的實現(xiàn)原理

? ??????阻塞隊列使用通知模式實現(xiàn),即當生產(chǎn)者往滿的隊列里添加元素時會阻塞生產(chǎn)者線程演侯,當消費者消費了隊列中一個元素后,會通知生產(chǎn)者當前隊列可用背亥。?

????????以下為ArrayBlockingQueue源碼:?

? ? ? ? 當往隊列插入一個元素秒际,如果隊列不可用,那么阻塞生產(chǎn)者主要通過LockSupport.park(this)來實現(xiàn)狡汉。

6.4? Fork/Join框架?

? ??6.4.1? Fork/Join框架的定義?

????????Fork/Join框架是Java 7提供的一個用于并行執(zhí)行任務的框架娄徊,它可以把大任務分割成若干個小任務,最終匯總每個小任務結果后得到大任務結果的框架盾戴。 其運行流程如下:?

? ??6.4.2? 工作竊取算法?

????????工作竊取算法是指某個線程從其他隊列里竊取任務來執(zhí)行寄锐。當我們需要做一個大任務時,可以把這個任務分割成若干互不依賴的子任務尖啡,為了減少線程競爭橄仆,把子任務分別放入不同隊列,并為每個隊列創(chuàng)建一個單獨的工作線程衅斩,假設盆顾,有其他線程提前把自己隊列任務做完之后,還需要等待其他線程畏梆,這時您宪,為了提高效率,已經(jīng)做完任務的線程會去其他隊列竊取任務執(zhí)行奠涌,以幫助其他未完成的線程宪巨。此時他們訪問同一個隊列,為了減少竊取任務線程和被竊取任務線程之間的競爭铣猩,通常會使用雙端隊列揖铜。被竊取任務線程永遠從雙端隊列頭部取任務執(zhí)行,竊取任務線程永遠從雙端隊列尾部拿任務執(zhí)行达皿。其運行流程圖如下:

? ? ? ?工作竊取算法優(yōu)點:就是充分利用線程進行并行計算天吓,減少了線程間的競爭。?

? ? ? ? 工作竊取算法缺點:當雙端隊列里只有一個任務時峦椰,還時會存在競爭龄寞。而且該算法創(chuàng)建多個線程和多個雙端隊列,會消耗更過的系統(tǒng)資源汤功。?

? ??6.4.3? Fork/Join框架的設計?

????Fork/Join框架有以下兩個步驟:?

????????- 分割任務:首先我們需要有一個fork類來把大任務分割成子任務物邑,如果子任務仍然很大,還需要不停分割,直到分割出的子任務足夠小色解。?

????????- 執(zhí)行任務并合并結果:分割的子任務分別放在雙端隊列茂嗓,然后啟動線程分別從隊列取任務執(zhí)行,執(zhí)行完的結果統(tǒng)一放在一個隊列里科阎,啟動一個線程從隊列里拿數(shù)據(jù)并合并數(shù)據(jù)述吸。?

????Fork/Join使用兩個類來完成上述工作:?

????????- ForkJoinTask:要使用ForkJoin框架,必須先創(chuàng)建一個ForkJoin任務锣笨。它提供在任務中執(zhí)行fork和join操作機制蝌矛,在使用時,我們通常繼承ForkJoinTask的子類:RecursiveAction或RecursiveTask错英,兩者區(qū)別是RecursiveAction用于沒有返回結果的任務入撒,RecursiveTask用于有返回結果的任務。?

????????- ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執(zhí)行椭岩。被分割出來的子任務會添加到當前工作線程所維護的雙端隊列中茅逮,進入隊列的頭部。當一個工作線程的隊列里暫時沒有任務時簿煌,它會隨機從其他工作線程的隊列的尾部竊取一個任務執(zhí)行氮唯。?

? ??6.4.4? 使用Fork/Join框架

? ??6.4.5??Fork/Join框架的異常處理?

????????ForkJoinTask在執(zhí)行時可能拋出異常,但我們無法再主線程里捕捉異常姨伟,但可以通過ForkJoinTask的isCompletedAbnnormally()方法來檢查任務是否已經(jīng)拋出異吵土穑或已經(jīng)被取消,調用getException()可以獲取異常夺荒,此方法返回Throwable對象瞒渠,如果任務取消則返回CancellationException。如果任務沒有完成或者沒有拋出異常則返回null技扼。?

? ??6.4.6??實現(xiàn)原理?

????????ForkJoinPool由ForkJoinTask數(shù)組和ForkJoinWorkerThread數(shù)組組成伍玖,F(xiàn)orkJoinTask數(shù)組負責存放程序提交給ForkJoinPool的任務,而ForkJoinWorkerThread數(shù)組負責執(zhí)行這些任務剿吻。?

(1)ForkJoinTask的fork方法實現(xiàn)原理?

????當我們調用ForkJoinTask的fork方法時窍箍,程序會調用ForkJoinWorkerThread的pushTask方法異步地執(zhí)行這個任務,然后立即返回結果丽旅。?

? ? ? pushTask方法把當前任務存放在ForkJoinTask數(shù)組隊列里椰棘,然后調用ForkJoinPool的signalWork()方法喚醒或創(chuàng)建一個工作線程來執(zhí)行任務。?

????(2)ForkJoinTask的join方法實現(xiàn)原理?

????????Join方法的主要作用是阻塞當前線程并等待獲取結果榄笙。

????????首先邪狞,它調用doJoin()方法獲取當前任務狀態(tài),任務有4中狀態(tài):已完成(NORMAL),被取消(CANCELLED),信號(SIGNAL)和出現(xiàn)異常(EXCEPTIONAL)茅撞。如果任務狀態(tài)是已完成帆卓,則直接返回任務結果巨朦。如果任務被取消,則拋出CancellationException剑令。如果任務拋出異常糊啡,則直接拋出對應異常。?

????????doJoin()方法源代碼:?

????????在doJoin()方法中尚洽,會查看任務狀態(tài)悔橄,如果任務已經(jīng)執(zhí)行完成靶累,則直接返回任務狀態(tài)腺毫,如果沒有執(zhí)行完成,則從任務數(shù)組里取出任務并執(zhí)行挣柬。如果順利執(zhí)行完畢潮酒,則將任務狀態(tài)設置為NORMAL,如果出現(xiàn)異常邪蛔,則將任務狀態(tài)設置為EXCEPTION急黎。

? ? 6.5? 本章小結


第7章? Java中的13個原子操作類

? ??????當一個線程更新一個變量時,程序如果沒有正確的同步侧到,那么這個變量對于其他線程來說是不可見的勃教。我們通常使用synchronized或者volatile來保證線程安全的更新共享變量。在JDK1.5中匠抗,提供了java.util.concurrent.atomic包故源,這個包中的原子操作類提供了一種用法簡單,性能高效汞贸,線程安全地更新一個變量的方式绳军。?

????????Atomic包里一共提供了13個類,有4種類型的原子更新方式:原子更新基本類型矢腻、原子更新數(shù)組门驾、原子更新引用和原子更新屬性。其實現(xiàn)基本都是使用Unsafe實現(xiàn)的包裝類多柑。?

7.1? 原子更新基本類型類?

????????- AtomicBoolean:原子更新布爾類型?

????????- AtomicInteger:原子更新整型?

????????- AtomicLong:原子更新長整型?

????????以上3個類提供的方法基本一致奶是,我們以AtomicInteger為例進行分析。?

? ??AtomicInteger常用的方法有:?

????????- int addAndGet(int delta):以原子方式將輸入的數(shù)值與實例中的值相加竣灌,并返回結果聂沙。?

????????- boolean compareAndSet(int expect,int upate):如果輸入的數(shù)值等于預期值,則以原子方式將該值設置為輸入的值帐偎。?

????????- int getAndIncrement():以原子方式將當前值加1逐纬,返回自增前的值。?

????????- void lazySet(int newValue):最終會設置成new Value,但可能導致其他線程在之后的一小段時間內還是可以讀到舊的值削樊。?

????????- int getAndSet(int newValue):以原子方式設置為newValue,并返回舊值豁生。

? ??其實現(xiàn)依靠我們熟悉的CAS算法:?

????????在Java的基本類型中除了Atomic包中提供原子更新的基本類型外兔毒,還由char、float和double甸箱。那么這些在Atomic包中沒有提供原子更新的基本類型怎么保證其原子更新呢??

????????從AtomicBoolean源碼中我們可以得到答案:首先將Boolean轉換為整型育叁,然后使用comareAndSwapInt進行CAS,所以原子更新char芍殖、float豪嗽、double同樣可以以此實現(xiàn)。

7.2? 原子更新數(shù)組?

????????- AtomicIntegerArray:原子更新整型數(shù)組里的元素豌骏。?

????????- AtomicLongArray:原子更新長整型數(shù)組里的元素龟梦。?

????????- AtomicReferenceArray:原子更新引用類型數(shù)組里的元素。?

? ??【備注:】看書上說原子更新數(shù)組有4個類窃躲,除了上述3個外计贰,還有AtomicBooleanArray類,但我在jdk5/6/7/8中都沒有找到這個類的存在蒂窒,只找到共12個原子操作類躁倒,而不是標題中的13個。不知道是否是書中的錯誤洒琢?請知情的童鞋不吝賜教秧秉。

????AtomicIntegerArray類主要提供原子的方式更新數(shù)組里的整型,其常用方法如下:?

????????- int addAndGet(int i,int delta):以原子方式將輸入值與數(shù)組中索引i的元素相加衰抑。?

????????- boolean compareAndSet(int i,int expect,int update):如果當前值等于預期值象迎,就把索引i的元素設置成update值。?

【備注】:在AtomicIntegerArray構造方法中停士,AtomicIntegerArray會將數(shù)組復制一份挖帘,所以當其對內數(shù)組元素進行修改時,不會影響原傳入數(shù)組恋技。?

7.3? 原子更新引用類型?

????????原子更新基本類型的AtomicInteger,只能更新一個變量拇舀,如果需要原子更新多個變量,就需要使用這個原子更新引用類型提供的類蜻底。Atomic包提供以下3個類:?

????????- AtomicReference:原子更新引用變量骄崩。?

????????- AtomicReferenceFieldUpdater:原子更新引用類型里的字段。?

????????- AtomicMarkableReference:原子更新帶有標記位的引用類型 薄辅∫鳎可以原子更新一個布爾類型的標記位和引用類型。構造方法時AtomicMarkableReference(V initialRef,boolean initialMark)站楚。

7.4? 原子更新字段類?

????????如果需要原子的更新某個類里的某個字段脱惰,就需要使用原子更新字段類,Atomic包提供了以下3個類進行原子字段更新:?

????????- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器窿春。?

????????- AtomicLongFiledUpdater:原子更新長整型字段的更新器拉一。?

????????- AtomicStampedReference:原子更新帶有版本號的引用類型采盒。該類將整數(shù)值與引用關聯(lián)起來,用于原子的更新數(shù)據(jù)和數(shù)據(jù)的版本號蔚润,避免CAS的ABA問題磅氨、?

????????想要原子的更新字段類需要調用靜態(tài)方法newUpdater()創(chuàng)建一個更新器,并設置想要更新的類和屬性嫡纠。且更新類的字段必須使用public volatile修飾符烦租。

7.5? 本章小結


第8章? Java中的并發(fā)工具類

????????在JDK的并發(fā)包里提供了幾個非常有用的工具類。CountDownLatch除盏、CyclicBarrier和Semaphore工具類提供了一種并發(fā)流程控制的手段叉橱,Exchanger工具類提供了在線程間交換數(shù)據(jù)的一種手段。?

8.1? 等待多線程完成的CountDownLatch

????????CountDownLatch允許一個或多個線程等待其他線程完成操作痴颊。?

????????在JDK1.5之前赏迟,我們要達到一個或多個線程等待其他線程完成操作,需要使用thread.join()方法蠢棱,不停檢查等待的線程是否存活。而CountDownLatch的出現(xiàn)給我們提供了一種功能更強大甩栈、更優(yōu)雅的方法泻仙。?

????????CountDwonLatch的構造方法結構一個int類型的參數(shù)作為計數(shù)器的值,如果需要等待N個線程完成工作量没,那就傳入N玉转。?

????????調用countDown()方法,計數(shù)器值會減1殴蹄,當計數(shù)器值為0時究抓,會喚醒被await阻塞的線程。CountDownLatch同樣提供超時時間的await(long time,TimeUnit unit)袭灯。如果到了指定的時間刺下,計數(shù)器仍然不為0,則同樣會喚醒線程稽荧。?

? ??????【備注】:CountDownLatch不能重新初始化或修改其內部計數(shù)器的值橘茉。

8.2? 同步屏障CyclicBarrier?

????????CyclicBarrer的作用是讓一組線程達到一個屏障(同步點)時被阻塞,直到所有的線程到達此屏障時姨丈,才會喚醒被屏障阻塞的所有線程畅卓。

????????CyclicBarrier默認構造方法CyclicBarrier(int parties),需要傳入屏障需要攔截的線程數(shù)量蟋恬。每個線程通過調用await方法告訴CyclicBarrier已經(jīng)到達屏障翁潘,隨后被阻塞,直到所有線程到達屏障或線程被中斷才被喚醒歼争。?

????????CyclicBarrier還提供一個更高級的構造函數(shù)CyclicBarrier(int parites,Runnable barrierAction),用于在所有線程到達屏障并被喚醒時拜马,優(yōu)先執(zhí)行barrierAction箱歧。

? ??????CyclicBarrier和CounDownLatch的區(qū)別?

????????????- CountDownLatch的計數(shù)器只能使用一次,而CyclicBarrier計數(shù)器可以使用reset()方法重置一膨。?

????????????- CyclicBarrier還提供獲取阻塞線程數(shù)量及檢測阻塞線程是否被中斷等CountDownLatch沒有的方法呀邢。?

? ??????【備注】: CountDownLatch的getCount()方法返回還有多少需要調用countDown方法去tryReleaseShared使計數(shù)器歸0的線程數(shù)量。CyclicBarrier的getParties()方法同樣返回需要到達屏障的線程數(shù)量豹绪。

8.3? 控制并發(fā)線程數(shù)的Semaphore?

????????Semaphore(信號量)是用來控制同時訪問特定資源的線程數(shù)量价淌,它通過協(xié)調各個線程,以保證合理的使用公共資源瞒津。?

????????Semaphore(2)表示允許2個線程獲取許可證蝉衣,也就是最大并發(fā)數(shù)為2。通過調用acquire()方法獲取許可證巷蚪,使用完畢之后調用release()方法歸還許可證病毡。?

????????除此之外,Semaphore還提供一些其他方法:?

????????????- intavailablePermits():返回此信號量中當前可用的許可證數(shù)屁柏。?

????????????- intgetQueueLength():返回正在等待獲取許可證的線程數(shù)啦膜。?

????????????- booleanhasQueuedThreads():是否有線程正在等待獲取許可證。?

????????????- void reducePermits(int reduction):減少reduction個許可證淌喻,是個protected方法僧家。?

????????????- Collection getQueuedThreads():返回所有等待獲取許可證的線程集合,是個protected方法裸删。

8.4? 線程間交換數(shù)據(jù)的Exchanger?

????????Exchanger(交換者)是一個用于線程協(xié)作的工具類八拱。用于進行線程間的數(shù)據(jù)交換。Exchanger提供一個同步點涯塔,在這個同步點肌稻,兩個線程可以交換彼此的數(shù)據(jù)。線程通過調用Exchanger的exchange()方法來通知Exchanger已經(jīng)到達同步點匕荸,并被阻塞直到另外一個線程也調用exchange()方法到達同步點時爹谭,兩個線程才可以交換數(shù)據(jù)。

8.5? 本章小結


第9章? Java中的線程池

????????Java中的線程池是運營場景最多的并發(fā)框架每聪,幾乎所有需要異步或并發(fā)執(zhí)行任務的程序都可以使用線程池旦棉,在開發(fā)過程中浦妄,合理地使用線程池能夠帶來3個好處:?

????????- 降低資源消耗:通過重復利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗缤弦。?

????????- 提高響應速度:當任務到達時肥照,任務可以不需要等待線程創(chuàng)建就可以立即執(zhí)行欢际。?

????????- 提高線程的可管理性:線程時稀缺資源假勿,如果無限制地創(chuàng)建原探,不僅會消耗系統(tǒng)資源誊役,還會降低系統(tǒng)的穩(wěn)定性倍试,使用線程池可以進行統(tǒng)一分配穷娱、調優(yōu)和監(jiān)控绑蔫。?

9.1? 線程池的實現(xiàn)原理?

????????當提交一個新任務到線程池時运沦,線程池的處理流程如下:?

????????(1) 線程池判斷核心線程池里的線程是否都在執(zhí)行任務,如果不是配深,則創(chuàng)建一個新的工作線程來執(zhí)行任務携添。如果核心線程池里的線程都在執(zhí)行任務,則進入下個流程篓叶。?

???????(2) 線程池判斷工作隊列是否已經(jīng)滿烈掠。如果沒滿,則將任務放入工作隊列缸托,等待核心線程池有空閑線程時左敌,再取出來執(zhí)行。如果滿了俐镐,則進入下個流程矫限。?

????????(3) 線程池判斷線程池的線程是否都處于工作狀態(tài),如果沒有佩抹,則創(chuàng)建一個新的工作線程來執(zhí)行任務叼风。如果滿了,則交給飽和策略取處理這個任務匹摇。?

????????流程圖如下:?

線程池的主要處理流程


????????ThreadPoolExecutor執(zhí)行execute方法分下面4種情況咬扇。

????????????1)如果當前運行的線程少于corePoolSize,則創(chuàng)建新線程來執(zhí)行任務(注意廊勃,執(zhí)行這一步驟需要獲取全局鎖)。

????????????2)如果運行的線程等于或多于corePoolSize经窖,則將任務加入BlockingQueue坡垫。

????????????3)如果無法將任務加入BlockingQueue(隊列已滿),則創(chuàng)建新的線程來處理任務(注意画侣,執(zhí)行這一步驟需要獲取全局鎖)冰悠。

????????????4)如果創(chuàng)建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕配乱,并調用RejectedExecutionHandler.rejectedExecution()方法溉卓。? ? ? ? ?

????????ThreadPoolExecutor采取上述步驟的總體設計思路,是為了在執(zhí)行execute()方法時搬泥,盡可能地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸)桑寨。在ThreadPoolExecutor完成預熱之后(當前運行的線程數(shù)大于等于corePoolSize),幾乎所有的execute()方法調用都是執(zhí)行步驟2忿檩,而步驟2不需要獲取全局鎖尉尾。

? ? ? ? 源碼分析:

????工作線程:線程池創(chuàng)建線程時,會將線程封裝成工作線程Worker燥透,Worker在執(zhí)行完任務后沙咏,還會循環(huán)獲取工作隊列里的任務來執(zhí)行辨图。我們可以從Worker類的run()方法里看到這點。

ThreadPoolExecutor執(zhí)行任務示意圖

????????線程池中的線程執(zhí)行任務分兩種情況肢藐,如下故河。

????????????1)在execute()方法中創(chuàng)建一個線程時,會讓這個線程執(zhí)行當前任務吆豹。

????????????2)這個線程執(zhí)行完上圖中1的任務后鱼的,會反復從BlockingQueue獲取任務來執(zhí)行。

9.2? 線程池的使用

? ? 9.2.1??線程池的創(chuàng)建

? ? ? ? 我們可以通過ThreadPoolExecutor來創(chuàng)建一個線程池:

? ? ? ? 創(chuàng)建一個線程池需要輸入一下幾個參數(shù):

????????1)corePoolSize(線程池的基本大姓胺怼):當提交一個任務到線程池時鸳吸,線程池會創(chuàng)建一個線程來執(zhí)行任務,即使其他空閑的基本線程能夠執(zhí)行新任務也會創(chuàng)建線程速勇,等到需要執(zhí)行的任務數(shù)大于線程池基本大小時就不再創(chuàng)建晌砾。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創(chuàng)建并啟動所有基本線程烦磁。

????????2)runnableTaskQueue(任務隊列):用于保存等待執(zhí)行的任務的阻塞隊列养匈。可以選擇以下幾個阻塞隊列都伪。ArrayBlockingQueue:是一個基于數(shù)組結構的有界阻塞隊列呕乎,此隊列按FIFO(先進先出)原則對元素進行排序。LinkedBlockingQueue:一個基于鏈表結構的阻塞隊列陨晶,此隊列按FIFO排序元素猬仁,吞吐量通常要高于ArrayBlockingQueue。靜態(tài)工廠方法Executors.newFixedThreadPool()使用這個隊列SynchronousQueue先誉,一個不存儲元素的阻塞隊列湿刽。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀態(tài)褐耳,吞吐量通常要高于Linked-BlockingQueue诈闺,靜態(tài)工廠方法Executors.newCachedThreadPool使用了這個隊列。PriorityBlockingQueue:一個具有優(yōu)先級的無限阻塞隊列铃芦。

????????3)maximumPoolSize(線程池最大數(shù)量):線程池允許創(chuàng)建的最大線程數(shù)雅镊。如果隊列滿了,并且已創(chuàng)建的線程數(shù)小于最大線程數(shù)刃滓,則線程池會再創(chuàng)建新的線程執(zhí)行任務仁烹。值得注意的是,如果使用了無界的任務隊列這個參數(shù)就沒什么效果注盈。

????????4)ThreadFactory:用于設置創(chuàng)建線程的工廠晃危,可以通過線程工廠給每個創(chuàng)建出來的線程設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給線程池里的線程設置有意義的名字,代碼如下:new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

????????5)RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了僚饭,說明線程池處于飽和狀態(tài)震叮,那么必須采取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy鳍鸵,表示無法處理新任務時拋出異常苇瓣。在JDK 1.5中Java線程池框架提供了以下4種策略。AbortPolicy:直接拋出異常偿乖。CallerRunsPolicy:只用調用者所在線程來運行任務击罪。DiscardOldestPolicy:丟棄隊列里最近的一個任務,并執(zhí)行當前任務贪薪。DiscardPolicy:不處理媳禁,丟棄掉。當然画切,也可以根據(jù)應用場景需要來實現(xiàn)RejectedExecutionHandler接口自定義策略竣稽。如記錄日志或持久化存儲不能處理的任務。keepAliveTime(線程活動保持時間):線程池的工作線程空閑后霍弹,保持存活的時間毫别。所以,如果任務很多典格,并且每個任務執(zhí)行的時間比較短岛宦,可以調大時間,提高線程的利用率耍缴。TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS)砾肺、小時(HOURS)、分鐘(MINUTES)防嗡、毫秒(MILLISECONDS)债沮、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS本鸣,千分之一微秒)。

9.2.2??向線程池提交任務?

????????可以使用execute()或submit()方法向線程池提交任務硅蹦。?

????????execute()方法用于提交不需要返回值的任務荣德,所以無法判斷任務是否被線程池執(zhí)行成功。通過以下代碼可知execute()方法輸入的任務是一個Runnable類的實例童芹。

????????submit()方法用于提交需要返回值的任務涮瞻。線程池會返回一個future類型的對象,通過這個future對象可以判斷任務是否執(zhí)行成功假褪,并且可以通過future的get()方法來獲取返回值署咽,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間后立即返回宁否,這時候有可能任務沒有執(zhí)行完窒升。

? ??9.2.3? 關閉線程池

????????可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是遍歷線程池中的工作線程慕匠,然后逐個調用線程的interrupt方法來中斷線程饱须,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區(qū)別台谊,shutdownNow首先將線程池的狀態(tài)設置成STOP蓉媳,然后嘗試停止所有的正在執(zhí)行或暫停任務的線程,并返回等待執(zhí)行任務的列表锅铅,而shutdown只是將線程池的狀態(tài)設置成SHUTDOWN狀態(tài)酪呻,然后中斷所有沒有正在執(zhí)行任務的線程。

????????只要調用了這兩個關閉方法中的任意一個盐须,isShutdown方法就會返回true玩荠。當所有的任務都已關閉后,才表示線程池關閉成功丰歌,這時調用isTerminaed方法會返回true姨蟋。至于應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定立帖,通常調用shutdown方法來關閉線程池眼溶,如果任務不一定要執(zhí)行完,則可以調用shutdownNow方法晓勇。

? ??9.2.4? 合理地配置線程池

????????要想合理地配置線程池堂飞,就必須首先分析任務特性,可以從以下幾個角度來分析绑咱。

????????????任務的性質:CPU密集型任務绰筛、IO密集型任務和混合型任務。

????????????任務的優(yōu)先級:高描融、中和低铝噩。

????????????任務的執(zhí)行時間:長、中和短窿克。

????????????任務的依賴性:是否依賴其他系統(tǒng)資源骏庸,如數(shù)據(jù)庫連接。

????性質不同的任務可以用不同規(guī)模的線程池分開處理年叮。CPU密集型任務應配置盡可能小的線程具被,如配置N cpu +1個線程的線程池。由于IO密集型任務線程并不是一直在執(zhí)行任務只损,則應配置盡可能多的線程一姿,如2*N cpu 。混合型的任務叮叹,如果可以拆分艾栋,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執(zhí)行的時間相差不是太大衬横,那么分解后執(zhí)行的吞吐量將高于串行執(zhí)行的吞吐量裹粤。如果這兩個任務執(zhí)行時間相差太大,則沒必要進行分解蜂林∫K撸可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數(shù)。優(yōu)先級不同的任務可以使用優(yōu)先級隊列PriorityBlockingQueue來處理噪叙。它可以讓優(yōu)先級高的任務先執(zhí)行矮锈。

? ??????注意:如果一直有優(yōu)先級高的任務提交到隊列里,那么優(yōu)先級低的任務可能永遠不能執(zhí)行睁蕾。執(zhí)行時間不同的任務可以交給不同規(guī)模的線程池來處理苞笨,或者可以使用優(yōu)先級隊列,讓執(zhí)行時間短的任務先執(zhí)行子眶。依賴數(shù)據(jù)庫連接池的任務瀑凝,因為線程提交SQL后需要等待數(shù)據(jù)庫返回結果,等待的時間越長臭杰,則CPU空閑時間就越長粤咪,那么線程數(shù)應該設置得越大,這樣才能更好地利用CPU渴杆。

? ??建議使用有界隊列寥枝。有界隊列能增加系統(tǒng)的穩(wěn)定性和預警能力,可以根據(jù)需要設大一點兒磁奖,比如幾千囊拜。有一次,我們系統(tǒng)里后臺任務線程池的隊列和線程池全滿了比搭,不斷拋出拋棄任務的異常冠跷,通過排查發(fā)現(xiàn)是數(shù)據(jù)庫出現(xiàn)了問題,導致執(zhí)行SQL變得非常緩慢身诺,因為后臺任務線程池里的任務全是需要向數(shù)據(jù)庫查詢和插入數(shù)據(jù)的蔽莱,所以導致線程池里的工作線程全部阻塞,任務積壓在線程池里戚长。如果當時我們設置成無界隊列,那么線程池的隊列就會越來越多怠苔,有可能會撐滿內存同廉,導致整個系統(tǒng)不可用,而不只是后臺任務出現(xiàn)問題。當然迫肖,我們的系統(tǒng)所有的任務是用單獨的服務器部署的锅劝,我們使用不同規(guī)模的線程池完成不同類型的任務,但是出現(xiàn)這樣問題時也會影響到其他任務蟆湖。

? ??9.2.5? 線程池的監(jiān)控

????????如果在系統(tǒng)中大量使用線程池故爵,則有必要對線程池進行監(jiān)控,方便在出現(xiàn)問題時隅津,可以根據(jù)線程池的使用狀況快速定位問題诬垂。可以通過線程池提供的參數(shù)進行監(jiān)控伦仍,在監(jiān)控線程池的時候可以使用以下屬性结窘。

????????taskCount:線程池需要執(zhí)行的任務數(shù)量。

????????completedTaskCount:線程池在運行過程中已完成的任務數(shù)量充蓝,小于或等于taskCount隧枫。

????????largestPoolSize:線程池里曾經(jīng)創(chuàng)建過的最大線程數(shù)量。通過這個數(shù)據(jù)可以知道線程池是否曾經(jīng)滿過谓苟。如該數(shù)值等于線程池的最大大小官脓,則表示線程池曾經(jīng)滿過。

????????getPoolSize:線程池的線程數(shù)量涝焙。如果線程池不銷毀的話卑笨,線程池里的線程不會自動銷毀,所以這個大小只增不減纱皆。

????????getActiveCount:獲取活動的線程數(shù)湾趾。通過擴展線程池進行監(jiān)控∨刹荩可以通過繼承線程池來自定義線程池搀缠,重寫線程池的beforeExecute、afterExecute和terminated方法近迁,也可以在任務執(zhí)行前艺普、執(zhí)行后和線程池關閉前執(zhí)行一些代碼來進行監(jiān)控。例如鉴竭,監(jiān)控任務的平均執(zhí)行時間歧譬、最大執(zhí)行時間和最小執(zhí)行時間等。這幾個方法在線程池里是空方法搏存。

? ??????????protected void beforeExecute(Thread t, Runnable r) { }

9.3? 本章小結


第10章? Executor框架

? ? ? ? Java線程既是工作單元瑰步,也是執(zhí)行機制。從JDK 5開始,把工作單元和執(zhí)行機制分離開來。工作單元包括Runnable和Callable,而執(zhí)行機制有Executor框架提供墨缘。

10.1??Executor框架簡介

????10.1.1??Executor框架的兩級調度模型

? ??????在HotSpot VM的線程模型中袁滥,Java線程被一對一映射為本地操作系統(tǒng)線程盖桥。Java線程啟動時會創(chuàng)建一個本地操作系統(tǒng)線程。當該Java線程終止時题翻,這個操作系統(tǒng)線程也會被回收揩徊。操作系統(tǒng)會調度所有線程并將它們分配給可用的CPU。?

????????在上層嵌赠,Java多線程程序通常會把應用分解成若干個任務塑荒,然后使用用戶級的調度器(Executor框架)將這些任務映射為固定數(shù)量的線程;在底層猾普,操作系統(tǒng)內核將這些線程映射到硬件處理器上袜炕。 其模型圖如下:?

? ? ? 1、Executor框架的結構?

????????Executor框架主要又3大部分組成:?

? ? ? ? ? ? 1. 任務:包括被執(zhí)行任務需要實現(xiàn)的接口:Runnable接口或Callable接口初家。?

????????????2. 任務的執(zhí)行:包括任務執(zhí)行機制的核心接口Executor,及其子接口ExecutorService接口偎窘。?Executor框架有兩個關節(jié)類實現(xiàn)了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)? ??

????????????3. 異步計算的結果:包括接口Future和其實現(xiàn)類FutureTask類。?

Executor框架的類與接口關系示意圖:?

????????下面是這些類和接口的簡介溜在。

????????Executor是一個接口陌知,它是Executor框架的基礎,它將任務的提交和任務的執(zhí)行分離開來掖肋。

????????ThreadPoolExecutor是線程池的核心實現(xiàn)類仆葡,用來執(zhí)行被提交的任務。

????????ScheduledThreadPoolExecutor是一個實現(xiàn)類志笼,可以在給定的延遲后運行命令沿盅,或者定期執(zhí)行命令。SchduledThreadPoolExecutor比Timer更靈活纫溃,功能更強大腰涧。

????????Future接口和實現(xiàn)Future接口的FutureTask類,代表異步計算的結果

????????Runnable接口和Callable接口的實現(xiàn)類紊浩,都可以被ThreadPoolExecutor或SchduledThreadPoolExecutor執(zhí)行

????2窖铡、Executor框架的成員

????????Executor框架的主要成員:ThreadPoolExecutor、ScheduledThreadPoolExecutor坊谁、Future接口费彼、Runnable接口、Callable接口口芍、Executors箍铲。

????????(1)ThreadPoolExecutor

????????ThreadPoolExecutor通常使用工廠類Executors來創(chuàng)建,Executors可以創(chuàng)建3種類型的ThreadPoolExecutor: SingleThreadExecutor鬓椭、FixedThreadPool虹钮、CachedThreadPool聋庵。

? ? ? ? ? ? 1) FixedThreadPool:創(chuàng)建固定線程數(shù)的線程池,構造函數(shù)中可以指定線程數(shù)量芙粱,適用于為了滿足資源管理的需求,而需要限制當前線程數(shù)量的應用場景氧映,它適用于負載比較重的服務器春畔。

????????????FixedThreadPool內部使用無界隊列LinkedBlockingQueue作為任務隊列,隊列的容量為Integer.MAX_VALUE岛都,由于是無界隊列律姨,所以不會拒絕任務,可能會造成任務無限堆積臼疫,從而導致系統(tǒng)資源耗盡的情況择份。

? ? ? ? ? ? 2) SingleThreadExecutor:創(chuàng)建單個線程的線程池,可以保證順序執(zhí)行任務烫堤。與FixedThreadPool類似荣赶,只是SingleThreadExecutor的線程數(shù)固定為1

? ? ? ? ? ? ?3) CachedThreadPool:可以根據(jù)需要創(chuàng)建新的線程,CachedThreadPool是大小無界的線程池鸽斟,適用于執(zhí)行很多短期異步任務的小程序拔创,或者是負載比較輕的服務器。CachedThreadPool的corePool為空富蓄,maximumPoolSize為Integer.MAX_VALUE剩燥,keepAliveTime為60L,這意味著線程空閑超過60秒則會進行回收立倍。CachedThreadPool內部使用不存儲元素的SynchronousQueue作為任務隊列(一個put操作等待著一個take操作)灭红,這意味著如果任務的提交速度高于線程的處理速度,那么CachedThreadPool則會不斷的創(chuàng)建新的線程口注,在極端的情況下变擒,會耗盡CPU和內存資源。? ? ? ? ? ??

????????(2) SchduledThreadPoolExecutor

????????SchduledThreadPoolExecutor通常使用工廠類Executors來創(chuàng)建疆导,Executors可以創(chuàng)建2中類型的SchduledThreadPoolExecutor赁项,如下:

????????????????SchduledThreadPoolExecutor。包含若干個線程的SchduledThreadPoolExecutor澈段。

????????????????SingleThreadSchduledExecutor悠菜。只包含一個線程的SchduledThreadPoolExecutor。

????????(3)Future接口

????????Future接口和實現(xiàn)Future接口的FutureTask類用來表示異步計算的結果败富,當我們把Runnable接口或者Callable接口的實現(xiàn)類提交(submit)給ThreadPoolExecutor或者SchduledThreadPoolExecutor時悔醋,ThreadPoolExecutor或者SchduledThreadPoolExecutor會向我們返回一個FutureTask對象。下面是對應的API

???? ? (4) Runnable和Callable接口

????????Runnable和Callable接口的實現(xiàn)類都可以被hreadPoolExecutor或者SchduledThreadPoolExecutor執(zhí)行兽叮。它們之間的區(qū)別是Runnable不會返回結果芬骄,而Callable可以返回結果猾愿。

????????除了可以自已創(chuàng)建實現(xiàn)Callable接口的對象外,還可以使用工廠類Executors來把一個Runnable包裝成一個Callable账阻。

????????當我們把一個Callable對象提交給ThreadPoolExecutor或者SchduledThreadPoolExecutor執(zhí)行時蒂秘,summit()會向我們返回一個FutureTask對象。我們可以執(zhí)行FutureTask.get()來等待任務執(zhí)行完成淘太。當任務完成后FutureTask.get()將會返回任務的結果姻僧。

10.2? ThreadPoolExecutor詳解

? ??????ThreadPoolExecutor是Executor框架最核心的類。主要由corePool(核心線程池大小)蒲牧、maximumPool(最大線程池大衅埠亍)、BlockingQueue(工作隊列)和RejecteExecutionHandler(飽和策略)構成冰抢。?

????????ThreadPoolExecutor通常使用Executors來創(chuàng)建松嘶。Executors可以創(chuàng)建3中類型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool挎扰。

? ??10.2.1? FixedThreadPool詳解?

? ??FixedThreadPool?被稱為可重用固定線程數(shù)的線程池翠订。? ?????

????????FixedThreadPool的corePoolSize和maximumPoolSize都被設置為創(chuàng)建FixedThreadPool時指定的參數(shù)nThreads。當線程池數(shù)大于corePoolSize時鼓鲁,keepAliveTime為多余空閑線程等待新任務的最長時間蕴轨,超過這個時間多余線程就會被終止。keepAliveTime設置為0L骇吭,意味著多余的空閑線程會被立即終止橙弱。 其運行示意圖如下:?

????????????1>如果當前運行的線程數(shù)少于corePoolSize,則創(chuàng)建新線程來執(zhí)行任務燥狰。?

????????????2>在線程池完成預熱之后(當前線程中有一個運行的線程)棘脐,將任務加入LinkedBlockingQueue。?

????????????3>線程執(zhí)行完1中的任務后龙致,會在一個無線循環(huán)中反復從LinkedBlockingQueue獲取任務來執(zhí)行蛀缝。?

????????FixedThreadPool使用無界隊列LinkedBlockingQueue作為線程池的工作隊列(隊列的容量為Integer.MAX_VALUE)。使用無界隊列作為工作隊列會對線程池帶來如下影響:?

????????????a>當線程池中的線程數(shù)達到corePoolSize后目代,新任務將在無界隊列中等待屈梁,因此線程池中的線程不會超過corePoolSize。?

????????????b>maximumPoolSize變?yōu)橐粋€無效參數(shù)榛了。?

????????????c>keepAliveTime也變?yōu)橐粋€無效參數(shù)在讶。?

????????????d>永遠不會執(zhí)行飽和策略。?

10.2.1 SingleThreadExecutor詳解

????????SingleThreadExecutor是使用單個worker線程的Executor霜大。?下面是其源代碼:?

????????SingleThreadExecutor的corePoolSize和maximumPoolSize被設置為1构哺。并使用LinkedBlockingQueue無界隊列作為工作隊列。 其運行示意圖如下:?

????????1>如果當前運行的線程數(shù)少于corePoolSize(即線程池中無運行的線程)战坤,則創(chuàng)建一個新線程來執(zhí)行任務曙强。?

????????2>在線程池完成預熱之后(當前線程中有一個運行的線程)残拐,將任務加入LinkedBlockingQueue。?

????????3>線程執(zhí)行完1中的任務后碟嘴,會在一個無線循環(huán)中反復從LinkedBlockingQueue獲取任務來執(zhí)行溪食。?

10.2.3 CachedThreadPool詳解

????????CacheThreadPool是一個會更加需要創(chuàng)建新線程的線程池。?下面是其源代碼:?

????????CachedThreadPool的corePoolSize被設置為0娜扇,即corePool為空眠菇;maximumPoolSize被設置為Integer.MAX_VALUE,即maximumPool時無界的袱衷。keepAliveTime被設置為60L,意味著空閑線程超過60秒后被終止笑窜。?

????????CachedThread使用沒有容量的SynchronousQueue作為線程池的工作隊列致燥,但其maximumPool是無界的。這意味著排截,如果主線程提交任務的速度高于maximumPool中線程處理任務的速度是嫌蚤,CachedThreadPool會不斷創(chuàng)建新的線程,極端情況下断傲,會耗盡CPU和內存資源脱吱。 其執(zhí)行示意圖如下:?

????????????1>首先執(zhí)行SynchronousQueue.offer(Runnable task)。如果當前有空閑線程正在執(zhí)行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECODES),則配對成功认罩,將任務交給空閑線程執(zhí)行箱蝠。?

????????????2>當沒有空閑線程時,創(chuàng)建一個新線程執(zhí)行任務垦垂。?

????????????3>線程在執(zhí)行任務完畢后宦搬,執(zhí)行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECODES),向隊列請求任務,并等待60秒劫拗。如果60之后仍沒有新任務间校,則被終止。如果有新任務則繼續(xù)執(zhí)行页慷。?

10.3??ScheduledThreadPoolExecutor?詳解

????????ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor憔足。它主要用來在給定的延遲之后運行任務,或定期執(zhí)行任務酒繁。?ScheduledThreadPoolExecutor通常使用工廠類Executors來創(chuàng)建滓彰。Executors可以創(chuàng)建2種類型的ScheduledThreadPoolExecutor:

????????????1>ScheduledThreadPoolExecutor適用于需要執(zhí)行周期任務,同時為了滿足資源管理的需求而限制后臺線程的數(shù)量的應用場景欲逃。?

????????????2>SingleThreadScheduledExecutor:只包含一個線程的ScheduledThreadPoolExecutor找蜜,適用于需要單個后臺線程執(zhí)行周期任務,同時需要保證順序地執(zhí)行各任務的應用場景稳析。?

10.3.1??ScheduledThreadPoolExecutor的運行機制

? ??? ? 其執(zhí)行示意圖如下:?

? ??????DelayQueue是一個無界隊列洗做,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中沒什么意義弓叛。ScheduledThreadPoolExecutor的執(zhí)行分為兩大部分:

????????????1>當調用ScheduledThreadPoolExecutor的scheduleAtFixedRate()或scheduleWithFixedDelay()時,會向ScheduledThreadPoolExecutor的DelayQueue添加一個實現(xiàn)了RunnableScheduledFutur接口的ScheduledFutureTask诚纸。

????????????2>線程池中的線程從DelayQueue中獲取ScheduledFutureTask并執(zhí)行撰筷。?

10.3.2??ScheduledThreadPoolExecutor的實現(xiàn)?

????????ScheduledThreadPoolExecutor把待調度的任務放在一個DelayQueue中。ScheduledFutureTask主要包含3個成員變量:?

????????????????long型變量 time畦徘,表示這個任務將要被執(zhí)行的具體時間毕籽。?

????????????????long型變量 sequenceNumbe,表示這個任務被添加到ScheduledThreadPoolExecutor中的序號井辆。?

????????????????long型成員變量 period关筒,表示任務執(zhí)行的間隔周期。?

????????DelayQueue封裝了一個PriorityQueue杯缺,這個PriorityQueue會對對列中的ScheduledFutureTask進行排序蒸播。time小的先執(zhí)行,被排在前面萍肆。如果兩個任務time相同則比較sequenceNumbe袍榆。?

????????ScheduledThreadPoolExecutor執(zhí)行任務的步驟如下:?

????????????????1>線程1從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大于等于當前時間塘揣。?

????????????????2>線程1執(zhí)行這個ScheduledFutureTask包雀。?

????????????????3>線程1修改ScheduledFutureTask的time變量為下次要被執(zhí)行的時間。?

????????????????4>線程1把修改time之后的ScheduledFutureTask放入DelayQueue(DelayQueue.add())亲铡。?

????????以下是DelayQueue.take()方法源碼:?

????????DelayQueue.add()方法源碼:?

10.4? FutureTask詳解

? ??????Future接口和實現(xiàn)Future接口的FutureTask類用來表示異步計算的結果才写。

? ??10.4.1? FutureTask?簡介

????????FutureTask除了實現(xiàn)Future接口外,還實現(xiàn)了Runnable接口奴愉。因此琅摩,F(xiàn)utureTask可以交給Executor執(zhí)行,也可以由調用線程直接執(zhí)行(FutureTask.run())锭硼。FutureTask有未啟動房资、已啟動、已完成3種狀態(tài)檀头。?

????????FutureTask的狀態(tài)遷移示意圖如下:?

????????1>未啟動轰异。FutureTask.run()方法還沒有被執(zhí)行之前,F(xiàn)utureTask處于未啟動狀態(tài)暑始。當創(chuàng)建一個FutureTask搭独,且沒有執(zhí)行FutureTask.run()方法之前,這個FutureTask處于未啟動狀態(tài)廊镜。?

????????2>已啟動牙肝。FutureTask.run()方法被執(zhí)行的過程中,F(xiàn)utureTask處于已啟動狀態(tài)。?

????????3>已完成配椭。FutureTask.run()方法執(zhí)行完后正常結束虫溜,或被取消(FutureTask.cancel()),或FutureTask.run()時拋出異常而異常結束股缸,F(xiàn)utureTask處于已完成狀態(tài)衡楞。?

? ??10.4.2? FutureTask的使用

? ??????????下面是FutureTask在不同狀態(tài)時調用FutureTask.get()及Future.cancel()方法的執(zhí)行示意圖:?

? ??10.4.3? FutureTask的實現(xiàn)

? ? ? ? FutureTask實現(xiàn)基于AQS.

10.5? 本章小結


第11章? Java并發(fā)編程實踐

????生產(chǎn)者和消費者模式

????線上問題定位

????性能測試

????異步任務池


完結……

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市敦姻,隨后出現(xiàn)的幾起案子瘾境,更是在濱河造成了極大的恐慌,老刑警劉巖镰惦,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迷守,死亡現(xiàn)場離奇詭異,居然都是意外死亡旺入,警方通過查閱死者的電腦和手機盒犹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來眨业,“玉大人,你說我怎么就攤上這事沮协×浼瘢” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵慷暂,是天一觀的道長聘殖。 經(jīng)常有香客問我,道長行瑞,這世上最難降的妖魔是什么奸腺? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮血久,結果婚禮上突照,老公的妹妹穿的比我還像新娘。我一直安慰自己氧吐,他們只是感情好讹蘑,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著筑舅,像睡著了一般座慰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翠拣,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天版仔,我揣著相機與錄音,去河邊找鬼。 笑死蛮粮,一個胖子當著我的面吹牛益缎,可吹牛的內容都是我干的。 我是一名探鬼主播蝉揍,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼链峭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了又沾?” 一聲冷哼從身側響起弊仪,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杖刷,沒想到半個月后励饵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡滑燃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年役听,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片表窘。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡典予,死狀恐怖,靈堂內的尸體忽然破棺而出乐严,到底是詐尸還是另有隱情瘤袖,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布昂验,位于F島的核電站捂敌,受9級特大地震影響昌执,放射性物質發(fā)生泄漏啄清。R本人自食惡果不足惜绽昏,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一暮刃、第九天 我趴在偏房一處隱蔽的房頂上張望选脊。 院中可真熱鬧鸣个,春花似錦榛斯、人聲如沸绊汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滞磺,卻和暖如春升薯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背击困。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工涎劈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留广凸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓蛛枚,卻偏偏與公主長得像谅海,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蹦浦,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內容