1. 前言
該書由Doug Lea之外的另外一位Java并發(fā)大神Brian Goetz和Tim Peierls合著壮韭,算是Java并發(fā)領(lǐng)域的一本經(jīng)典書籍。此書從2013年入手之后员串,拿起放下了三次。之前兩次自己對(duì)并發(fā)的研究還不是很深昼扛,基本屬于一知半解寸齐,工作當(dāng)中也極少用到并發(fā),看了就忘抄谐。最近半年在閱讀JDK源代碼渺鹦,特別是閱讀完部分java.util.concurrency
包之后,對(duì)并發(fā)的感覺更深蛹含。這個(gè)時(shí)候回頭來(lái)看看這本書毅厚,才真正體會(huì)到了其中的真諦,確實(shí)是字字珠璣浦箱。
本文記錄下自己閱讀完獲得的一些感悟吸耿,具體的API就不會(huì)在這里敘述,記錄更多設(shè)計(jì)和方法的部分憎茂,歡迎讀者一同探討珍语。
2. 當(dāng)我們討論線程問(wèn)題,我們?cè)谡f(shuō)什么
當(dāng)我們討論線程問(wèn)題時(shí)竖幔,其實(shí)關(guān)注的是兩個(gè)概念:可見性與原子性板乙。
可見性
可見性就是對(duì)于AB兩個(gè)線程共同操作了一個(gè)變量V,設(shè)計(jì)中A先修改了變量V;那么募逞,A對(duì)V的修改對(duì)B是否可見蛋铆。
在單線程環(huán)境中,如果向某個(gè)變量先寫入值放接,然后在沒有其他寫入操作的情況下讀取這個(gè)變量的值刺啦,那么總能得到相同的值。聽起來(lái)似乎是一個(gè)很簡(jiǎn)單的問(wèn)題纠脾,其實(shí)不然玛瘸。
在多線程和現(xiàn)代處理器的環(huán)境下,上述的過(guò)程就沒有那么的簡(jiǎn)單。
當(dāng)讀線程和寫線程在不同的線程中執(zhí)行時(shí),我們無(wú)法確保執(zhí)行讀操作的線程能適時(shí)的看到其他線程寫入的值麸粮。另外在現(xiàn)代處理器中,一個(gè)CPU通常包含多個(gè)核渺绒。變量V的修改并非直接在內(nèi)存中修改,而是現(xiàn)在某個(gè)核的寄存器和本地緩存中進(jìn)行修改菱鸥,然后再寫入到內(nèi)存中宗兼。這個(gè)時(shí)候,V的修改才會(huì)對(duì)其他核上的線程可見氮采。要實(shí)現(xiàn)這些功能殷绍,Java通過(guò)一系列的CPU指令幫我們除了了不同CPU廠商之間的實(shí)現(xiàn)差異。通過(guò)內(nèi)存屏障扳抽,在CPU處理到內(nèi)屏屏障時(shí)篡帕,強(qiáng)制將本地緩存中的變量值與其他CPU同步實(shí)現(xiàn)可見性的語(yǔ)義。
原子性
原子性就是保證操作是原子的贸呢,所有中間狀態(tài)都不會(huì)被其他線程訪問(wèn)到镰烧。
比如對(duì)一個(gè)Int的賦值x = 1;
可以認(rèn)為是原子的。但是自增操作x++;
就不是原子的楞陷,因?yàn)樾枰龡ljvm指令去完成它:1. 讀取變量x怔鳖;2. 對(duì)變量x加1;3.將結(jié)果賦值給變量x固蛾。如果在多線程環(huán)境下结执,因?yàn)闀r(shí)序的關(guān)系就可能導(dǎo)致x最終出現(xiàn)多個(gè)值。那么這個(gè)時(shí)候艾凯,自增操作就不能稱為是原子的献幔,非原子的狀態(tài)操作就是線程不安全的。
2.1 安全性問(wèn)題
在前面我們提到了趾诗,如果多線程環(huán)境下不滿足可見性和原子性蜡感,就會(huì)發(fā)生線程不安全蹬蚁,那到底什么是線程安全呢?在書中這么定義:
當(dāng)多個(gè)線程訪問(wèn)某個(gè)類時(shí)郑兴,不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行犀斋,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)調(diào),這個(gè)類都能表現(xiàn)出正確的行為情连,那么就稱這個(gè)類是線程安全的叽粹。
在這個(gè)定義下有這么幾個(gè)描述需要進(jìn)一步說(shuō)明:1)正確的行為;2)主調(diào)代碼中不需要任何額外的同步或協(xié)調(diào)却舀。
2.1.1 正確的行為
什么是正確的行為虫几?正確的行為就是符合需求規(guī)范定義的行為,這些行為的描述一般都是遵照時(shí)間順序的禁筏,比如:
如果事件A先發(fā)生持钉,對(duì)變量X復(fù)制1衡招;事件B后發(fā)生篱昔,對(duì)變量X復(fù)制2;那么X的值應(yīng)該是2
上面的一個(gè)描述就是一個(gè)** Requirement Specification 需求規(guī)范 ** 始腾。這些描述通常都是遵循 ** Sequential Consistency 順序一致性 **州刽,滿足人腦的一般思維模式(思考事情的時(shí)候不會(huì)從平行宇宙,多維時(shí)空角度來(lái)想這件事情該怎么做)浪箭。
2.1.2 額外的同步或協(xié)調(diào)
我們說(shuō)一個(gè)類不是線程安全的穗椅,應(yīng)該指的是這個(gè)類在沒有額外的同步或協(xié)調(diào)下,會(huì)產(chǎn)生可見性和原子性等問(wèn)題奶栖,例如我們經(jīng)常說(shuō)HashMap是非線程安全的匹表,其實(shí)就是說(shuō)使用HashMap如果要達(dá)到可見性和原子性的要求。
我們通過(guò)Collections.synchronizedMap(hashmap)
進(jìn)行包裝之后宣鄙,hashmap就變成了線程安全的類袍镀。如果翻看源碼,其實(shí)Collections.synchronizedMap(hashmap)
所做的事情冻晤,就是加了一段裝飾而已:
synchronized(mutex){
hashmap.xxx();
}
如果將這段放在調(diào)用處苇羡,也可以讓一個(gè)HashMap編程線程安全,但就加上了額外的同步和協(xié)調(diào)鼻弧,就沒辦法說(shuō)明HashMap是線程安全的设江。
2.2 活躍性問(wèn)題
如果說(shuō)線程的安全性是指“永遠(yuǎn)不發(fā)生糟糕的事情”, 那么線程的活躍性就是指“某件正確的事情最終會(huì)發(fā)生”攘轩。當(dāng)某個(gè)操作無(wú)法繼續(xù)執(zhí)行下去時(shí)叉存,就會(huì)發(fā)生活躍性問(wèn)題。在串行程序中度帮,表現(xiàn)為死循環(huán)——死循環(huán)外的正確的事情永遠(yuǎn)不會(huì)發(fā)生歼捏。
在多線程環(huán)境下,通常表現(xiàn)為阻塞或掛起。例如線程A等待線程B持有的某個(gè)資源甫菠,而線程B一直不釋放挠铲,那么A就會(huì)永久的等待下去。還有包括死鎖寂诱、饑餓拂苹、活鎖等,這些都屬于活躍性問(wèn)題痰洒。
2.3 性能問(wèn)題
如果說(shuō)線程的活躍性問(wèn)題是指“某件正確的事情最終會(huì)發(fā)生”瓢棒,那么性能問(wèn)題就是指“某件正確的事情最終會(huì)發(fā)生,應(yīng)該盡快發(fā)生”丘喻。性能問(wèn)題包含很多方面脯宿,如服務(wù)時(shí)間過(guò)長(zhǎng)、響應(yīng)不靈敏泉粉、吞吐率過(guò)低连霉、資源消耗過(guò)高或者伸縮性較低等。
3. Java為我們提供的線程安全基礎(chǔ)設(shè)施有哪些嗡靡?
Java 自1.5開始跺撼,提供了功能強(qiáng)大的java.util.concurrency
包,其中提供了大量的并發(fā)工具供不了解并發(fā)的我們使用讨彼,幫助構(gòu)建線程安全的程序歉井。當(dāng)然,里面大部分是Doug Lea大神寫的哈误。
3.1 內(nèi)置鎖
看新的東西之前哩至,還是要看到Java提供的 synchronized關(guān)鍵字。自1.2版本開始Java就把該關(guān)鍵詞作為了最基礎(chǔ)的同步機(jī)制蜜自,稱為內(nèi)置鎖菩貌。內(nèi)置鎖可以作用在方法、代碼塊中袁辈,作用在方法時(shí)表示用該類的當(dāng)前實(shí)例作為鎖對(duì)象加鎖菜谣。
如果線程(注意,討論的對(duì)象是線程)需要訪問(wèn)實(shí)例的同步方法晚缩,則需要先獲取實(shí)例的內(nèi)置鎖尾膊,在執(zhí)行完成后自動(dòng)釋放。如果該實(shí)例的鎖已經(jīng)被另外的線程獲取荞彼,則當(dāng)前線程會(huì)在該鎖上排隊(duì)等待冈敛,等待之前一個(gè)線程釋放鎖。鎖的獲取與釋放都通過(guò)編譯器加入monitor_enter
和montior_exit
指令實(shí)現(xiàn)鸣皂。
內(nèi)置鎖一度是java中進(jìn)行同步的唯一方法抓谴,很多遺留方法還是使用了內(nèi)置鎖進(jìn)行同步暮蹂,比如著名的Vertex,Collections里面的同步包裝器等癌压。在1.6之后仰泻,內(nèi)置鎖的性能也得到了很大的提升,在還未具備很強(qiáng)的并發(fā)經(jīng)驗(yàn)之前滩届,還是應(yīng)該優(yōu)先選擇內(nèi)置鎖集侯。
之所以之后,會(huì)產(chǎn)生更多的工具來(lái)替代內(nèi)置鎖的部分功能帜消,主要是因?yàn)閮?nèi)置鎖的最大缺點(diǎn)——無(wú)法控制棠枉。上面我們說(shuō)了,內(nèi)置鎖是通過(guò)編譯器和JVM指令實(shí)現(xiàn)的泡挺,因此在程序員角度無(wú)法對(duì)加鎖和解鎖行為進(jìn)行太多的控制辈讶;另外內(nèi)置鎖也是不可中斷,而且錯(cuò)綜復(fù)雜的調(diào)用關(guān)系會(huì)讓內(nèi)置鎖的加鎖組合娄猫、加鎖順序變得難以管理贱除。因此,Java在1.5中加入了Lock接口和對(duì)應(yīng)實(shí)現(xiàn)稚新,稱為顯示鎖勘伺。
3.2 顯示鎖(Lock, Condition, 條件謂語(yǔ))
顯式鎖的頂層接口為L(zhǎng)ock,提供了ReenterantLock, ReadWriteLock等實(shí)現(xiàn)褂删。
使用Lock的一個(gè)模式如下:
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新對(duì)象狀態(tài)
// 捕獲異常,并在必要時(shí)回復(fù)不變性條件
} finally {
lock.unlock();
}
3.3 信號(hào)量冲茸、柵欄屯阀、閉鎖
Lock本質(zhì)上開始一個(gè)開閉鎖,只有開閉兩個(gè)狀態(tài)轴术。在應(yīng)用中难衰,通常還會(huì)衍生出很多其他的需求。比如一個(gè)并行計(jì)算的需求逗栽,存在一個(gè)100萬(wàn)個(gè)數(shù)據(jù)集合盖袭,求這100萬(wàn)個(gè)數(shù)的和。單線程的場(chǎng)景就是把這些數(shù)從第一個(gè)加到最后一個(gè)彼宠,最后輸出鳄虱。在多核環(huán)境下,我們可以將這個(gè)問(wèn)題并行化凭峡,把100萬(wàn)數(shù)據(jù)分成100份每份1萬(wàn)數(shù)據(jù)拙已,然后由一個(gè)線程進(jìn)行計(jì)算,最后將這100個(gè)線程的計(jì)算結(jié)果相加就是這100萬(wàn)個(gè)數(shù)據(jù)的最終結(jié)果摧冀。
由于調(diào)度機(jī)制的原因倍踪,這100個(gè)線程可能會(huì)以任意的順序結(jié)束系宫,但只有這100個(gè)線程全部完成的時(shí)候我們才能獲得最終結(jié)果。因此需要一定的協(xié)調(diào)機(jī)制建车,在100個(gè)線程都結(jié)束時(shí)扩借,調(diào)用最后的結(jié)果輸出程序。這個(gè)需求都可以通過(guò)信號(hào)量缤至、柵欄往枷、閉鎖等實(shí)現(xiàn)。具體實(shí)現(xiàn)等有時(shí)間再寫一篇文章介紹(又給自己挖了坑)凄杯。
3.4 Non-blocking算法和Lock free算法
我們?cè)?.2節(jié)中提到了活躍性和性能错洁,通過(guò)同步處理的多線程問(wèn)題,都或多或少的影響了活躍性和性能戒突。比如線程A獲得了一個(gè)鎖并在執(zhí)行一些耗時(shí)的計(jì)算屯碴,如果線程B同樣想獲得這個(gè)鎖,那么程序的活躍性和性能就收到了影響膊存。
java 1.5之后导而,jvm開始支持硬件的CAS(Compare and Swap)指令,CAS接受三個(gè)參數(shù)(variable, expectedValue, newValue)隔崎。CAS的語(yǔ)義是這樣的今艺,如果變量variable的值和expectedValue相等,那么就將variable賦值為newValue爵卒;如果和expectedValue不相等虚缎,就返回失敗。
jdk1.5 使用CAS操作引入了AtomicInteger等一些列原子量钓株,可以保證“先檢查再修改”引起的多線程安全問(wèn)題实牡。聽起來(lái)很高大上,其實(shí)就是一個(gè)樂觀鎖的思路轴合,假設(shè)在當(dāng)前線程修改期間创坞,其他線程不會(huì)對(duì)原數(shù)據(jù)進(jìn)行修改。在寫入之前做一次檢查受葛,如果被修改了题涨,則進(jìn)行重試,重試到成功為止总滩。典型代碼如下:
while(true){
int old = getState(); // 1. 讀取舊制
int new = old + 1; // 2.對(duì)舊值做出修改
if(CAS(old, new){ //
return true;
}
}