關(guān)于高并發(fā)的一些思考

1.問(wèn)題

1、什么是線程的交互方式?

2、如何區(qū)分線程的同步/異步,阻塞/非阻塞?

3、什么是線程安全,如何做到線程安全?

4、如何區(qū)分并發(fā)模型?

5、何謂響應(yīng)式編程?

6、操作系統(tǒng)如何調(diào)度多線程?

2.關(guān)鍵詞

同步,異步,阻塞,非阻塞,并行,并發(fā),臨界區(qū),競(jìng)爭(zhēng)條件,指令重排,鎖,amdahl,gustafson

3.全文概要

上一篇我們介紹分布式系統(tǒng)的知識(shí)體系,由于單機(jī)的性能上限原因我們才不得不發(fā)展分布式技術(shù)。那么話說(shuō)回來(lái),如果單機(jī)的性能沒(méi)能最大限度的榨取出來(lái),就盲目的就建設(shè)分布式系統(tǒng),那就有點(diǎn)本末倒置了。而且上一篇我們給的忠告是如果有可能的話,不要用分布式,意思是說(shuō)如果單機(jī)性能滿足的話,就不要折騰復(fù)雜的分布式架構(gòu)。如果說(shuō)分布式架構(gòu)是宏觀上的性能擴(kuò)展,那么高并發(fā)則是微觀上的性能調(diào)優(yōu),這也是上一篇性能部分拆出來(lái)的大專題。本文將從線程的基礎(chǔ)理論談起,逐步探究線程的內(nèi)存模型,線程的交互,線程工具和并發(fā)模型的發(fā)展。掃除關(guān)于并發(fā)編程的諸多模糊概念,從新構(gòu)建并發(fā)編程的層次結(jié)構(gòu)。

4.基礎(chǔ)理論

4.1基本概念

開(kāi)始學(xué)習(xí)并發(fā)編程前,我們需要熟悉一些理論概念。既然我們要研究的是并發(fā)編程,那首先應(yīng)該對(duì)并發(fā)這個(gè)概念有所理解才是,而說(shuō)到并發(fā)我們肯定要要討論一些并行。

并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)

并行:多個(gè)處理器或者是多核的處理器同時(shí)處理多個(gè)不同的任務(wù)

然后我們需要再了解一下同步和異步的區(qū)別:

同步:執(zhí)行某個(gè)操作開(kāi)始后就一直等著按部就班的直到操作結(jié)束

異步:執(zhí)行某個(gè)操作后立即離開(kāi),后面有響應(yīng)的話再來(lái)通知執(zhí)行者

接著我們?cè)倭私庖粋€(gè)重要的概念:

臨界區(qū):公共資源或者共享數(shù)據(jù)

由于共享數(shù)據(jù)的出現(xiàn),必然會(huì)導(dǎo)致競(jìng)爭(zhēng),所以我們需要再了解一下:

阻塞:某個(gè)操作需要的共享資源被占用了,只能等待,稱為阻塞

非阻塞:某個(gè)操作需要的共享資源被占用了,不等待立即返回,并攜帶錯(cuò)誤信息回去,期待重試

如果兩個(gè)操作都在等待某個(gè)共享資源而且都互不退讓就會(huì)造成死鎖:

死鎖:參考著名的哲學(xué)家吃飯問(wèn)題

饑餓:饑餓的哲學(xué)家等不齊筷子吃飯

活鎖:相互謙讓而導(dǎo)致阻塞無(wú)法進(jìn)入下一步操作,跟死鎖相反,死鎖是相互競(jìng)爭(zhēng)而導(dǎo)致的阻塞

4.2并發(fā)級(jí)別

理想情況下我們希望所有線程都一起并行飛起來(lái)。但是CPU數(shù)量有限,線程源源不斷,總得有個(gè)先來(lái)后到,不同場(chǎng)景需要的并發(fā)需求也不一樣,比如秒殺系統(tǒng)我們需要很高的并發(fā)程度,但是對(duì)于一些下載服務(wù),我們需要的是更快的響應(yīng),并發(fā)反而是其次的。所以我們也定義了并發(fā)的級(jí)別,來(lái)應(yīng)對(duì)不同的需求場(chǎng)景。

阻塞:阻塞是指一個(gè)線程進(jìn)入臨界區(qū)后,其它線程就必須在臨界區(qū)外等待,待進(jìn)去的線程執(zhí)行完任務(wù)離開(kāi)臨界區(qū)后,其它線程才能再進(jìn)去。

無(wú)饑餓:線程排隊(duì)先來(lái)后到,不管優(yōu)先級(jí)大小,先來(lái)先執(zhí)行,就不會(huì)產(chǎn)生饑餓等待資源,也即公平鎖;相反非公平鎖則是根據(jù)優(yōu)先級(jí)來(lái)執(zhí)行,有可能排在前面的低優(yōu)先級(jí)線程被后面的高優(yōu)先級(jí)線程插隊(duì),就形成饑餓

無(wú)障礙:共享資源不加鎖,每個(gè)線程都可以自有讀寫,單監(jiān)測(cè)到被其他線程修改過(guò)則回滾操作,重試直到單獨(dú)操作成功;風(fēng)險(xiǎn)就是如果多個(gè)線程發(fā)現(xiàn)彼此修改了,所有線程都需要回滾,就會(huì)導(dǎo)致死循環(huán)的回滾中,造成死鎖

無(wú)鎖:無(wú)鎖是無(wú)障礙的加強(qiáng)版,無(wú)鎖級(jí)別保證至少有一個(gè)線程在有限操作步驟內(nèi)成功退出,不管是否修改成功,這樣保證了多個(gè)線程回滾不至于導(dǎo)致死循環(huán)

無(wú)等待:無(wú)等待是無(wú)鎖的升級(jí)版,并發(fā)編程的最高境界,無(wú)鎖只保證有線程能成功退出,但存在低級(jí)別的線程一直處于饑餓狀態(tài),無(wú)等待則要求所有線程必須在有限步驟內(nèi)完成退出,讓低級(jí)別的線程有機(jī)會(huì)執(zhí)行,從而保證所有線程都能運(yùn)行,提高并發(fā)度。

4.3量化模型

首先,多線程不意味著并發(fā),但并發(fā)肯定是多線程或者多進(jìn)程。我們知道多線程存在的優(yōu)勢(shì)是能夠更好的利用資源,有更快的請(qǐng)求響應(yīng)。但是我們也深知一旦進(jìn)入多線程,附帶而來(lái)的是更高的編碼復(fù)雜度,線程設(shè)計(jì)不當(dāng)反而會(huì)帶來(lái)更高的切換成本和資源開(kāi)銷。但是總體上我們肯定知道利大于弊,這不是廢話嗎,不然誰(shuí)還愿意去搞多線程并發(fā)程序,但是如何衡量多線程帶來(lái)的效率提升呢,我們需要借助兩個(gè)定律來(lái)衡量。

Amdahl

S=1/(1-a+a/n)

其中,a為并行計(jì)算部分所占比例,n為并行處理結(jié)點(diǎn)個(gè)數(shù)。這樣,當(dāng)1-a=0時(shí),(即沒(méi)有串行,只有并行)最大加速比s=n;當(dāng)a=0時(shí)(即只有串行,沒(méi)有并行),最小加速比s=1;當(dāng)n→∞時(shí),極限加速比s→ 1/(1-a),這也就是加速比的上限。

Gustafson

系統(tǒng)優(yōu)化某部件所獲得的系統(tǒng)性能的改善程度,取決于該部件被使用的頻率,或所占總執(zhí)行時(shí)間的比例。

兩面列舉了這兩個(gè)定律來(lái)衡量系統(tǒng)改善后提升效率的量化指標(biāo),具體的應(yīng)用我們?cè)谙挛牡木€程調(diào)優(yōu)會(huì)再詳細(xì)介紹。

5.內(nèi)存模型

宏觀上分布式系統(tǒng)需要解決的首要問(wèn)題是數(shù)據(jù)一致性,同樣,微觀上并發(fā)編程要解決的首要問(wèn)題也是數(shù)據(jù)一致性。貌似我們搞了這么多年的斗爭(zhēng)都是在公關(guān)一致性這個(gè)世界性難題。既然并發(fā)編程要從微觀開(kāi)始,那么我們肯定要對(duì)CPU和內(nèi)存的工作機(jī)理有所了解,尤其是數(shù)據(jù)在CPU和內(nèi)存直接的傳輸機(jī)制。

5.1整體原則

探究?jī)?nèi)存模型之前我們要拋出三個(gè)概念:

原子性

在32位的系統(tǒng)中,對(duì)于4個(gè)字節(jié)32位的Integer的操作對(duì)應(yīng)的JVM指令集映射到匯編指令為一個(gè)原子操作,所以對(duì)Integer類型的數(shù)據(jù)操作是原子性,但是Long類型為8個(gè)字節(jié)64位,32位系統(tǒng)要分為兩條指令來(lái)操作,所以不是原子操作。

對(duì)于32位操作系統(tǒng)來(lái)說(shuō),單次次操作能處理的最長(zhǎng)長(zhǎng)度為32bit,而long類型8字節(jié)64bit,所以對(duì)long的讀寫都要兩條指令才能完成(即每次讀寫64bit中的32bit)

可見(jiàn)性

線程修改變量對(duì)其他線程即時(shí)可見(jiàn)

有序性

串行指令順序唯一,并行線程直接指令可能出現(xiàn)不一致,也即是指令被重排了

而指令重排也是有一定原則(摘自《深入理解Java虛擬機(jī)第12章》):

程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;

鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作;

volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作;

傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;

線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作;

線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;

線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行;

對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始;

5.2邏輯內(nèi)存

我們談的邏輯內(nèi)存也即是JVM的內(nèi)存格局。JVM將操作系統(tǒng)提供的物理內(nèi)存和CPU緩存在邏輯分為堆,棧,方法區(qū),和程序計(jì)數(shù)器。在《從宏觀微觀角度淺析JVM虛擬機(jī)》 一文我們?cè)敿?xì)介紹了JVM的內(nèi)存模型分布,并發(fā)編程我們主要關(guān)注的是堆棧的分配,因?yàn)榫€程都是寄生在棧里面的內(nèi)存段,把棧里面的方法邏輯讀取到CPU進(jìn)行運(yùn)算。

5.3物理內(nèi)存

而實(shí)際的物理內(nèi)存包含了主存和CPU的各級(jí)緩存還有寄存器,而為了計(jì)算效率,CPU往往回就近從緩存里面讀取數(shù)據(jù)。在并發(fā)的情況下就會(huì)造成多個(gè)線程之間對(duì)共享數(shù)據(jù)的錯(cuò)誤使用。

5.4內(nèi)存映射

由于可能發(fā)生對(duì)象的變量同時(shí)出現(xiàn)在主存和CPU緩存中,就可能導(dǎo)致了如下問(wèn)題:

線程修改的變量對(duì)外可見(jiàn)

讀寫共享變量時(shí)出現(xiàn)競(jìng)爭(zhēng)資源

由于線程內(nèi)的變量對(duì)棧外是不可見(jiàn)的,但是成員變量等共享資源是競(jìng)爭(zhēng)條件,所有線程可見(jiàn),就會(huì)出現(xiàn)如下當(dāng)一個(gè)線程從主存拿了一個(gè)變量1修改后變成2存放在CPU緩存,還沒(méi)來(lái)得及同步回主存時(shí),另外一個(gè)線程又直接從主存讀取變量為1,這樣就出現(xiàn)了臟讀。

現(xiàn)在我們弄清楚了線程同步過(guò)程數(shù)據(jù)不一致的原因,接下來(lái)要解決的目標(biāo)就是如何避免這種情況的發(fā)生,經(jīng)過(guò)大量的探索和實(shí)踐,我們從概念上不斷的革新比如并發(fā)模型的流水線化和無(wú)狀態(tài)函數(shù)式化,而且也提供了大量的實(shí)用工具。接下來(lái)我們從無(wú)到有,先了解最簡(jiǎn)單的單個(gè)線程的一些特點(diǎn),弄清楚一個(gè)線程有多少能耐后,才能深刻認(rèn)識(shí)多個(gè)線程一起打交道會(huì)出現(xiàn)什么幺蛾子。

6.線程單元

6.1狀態(tài)

我們知道應(yīng)用啟動(dòng)體現(xiàn)的就是靜態(tài)指令加載進(jìn)內(nèi)存,進(jìn)而進(jìn)入CPU運(yùn)算,操作系統(tǒng)在內(nèi)存開(kāi)辟了一段棧內(nèi)存用來(lái)存放指令和變量值,從而形成了進(jìn)程。而其實(shí)我們的JVM也就是一個(gè)進(jìn)程而且,而線程是進(jìn)程的最小單位,也就是說(shuō)進(jìn)程是由很多個(gè)線程組成的。而由于進(jìn)程的上下文關(guān)聯(lián)的變量,引用,計(jì)數(shù)器等現(xiàn)場(chǎng)數(shù)據(jù)占用了打段的內(nèi)存空間,所以頻繁切換進(jìn)程需要整理一大段內(nèi)存空間來(lái)保存未執(zhí)行完的進(jìn)程現(xiàn)場(chǎng),等下次輪到CPU時(shí)間片再恢復(fù)現(xiàn)場(chǎng)進(jìn)行運(yùn)算。這樣既耗費(fèi)時(shí)間又浪費(fèi)空間,所以我們才要研究多線程。畢竟由于線程干的活畢竟少,工作現(xiàn)場(chǎng)數(shù)據(jù)畢竟少,所以切換起來(lái)比較快而且暫用少量空間。而線程切換直接也需要遵守一定的法則,不然到時(shí)候把工作現(xiàn)場(chǎng)破壞了就無(wú)法恢復(fù)工作了。

線程狀態(tài)

我們先來(lái)研究線程的生命周期,看看Thread類里面對(duì)線程狀態(tài)的定義就知道

public enum State {? ? /**

? ? * Thread state for a thread which has not yet started.

? ? */

? ? NEW,? ? /**

? ? * Thread state for a runnable thread.? A thread in the runnable

? ? * state is executing in the Java virtual machine but it may

? ? * be waiting for other resources from the operating system

? ? * such as processor.

? ? */

? ? RUNNABLE,? ? /**

? ? * Thread state for a thread blocked waiting for a monitor lock.

? ? * A thread in the blocked state is waiting for a monitor lock

? ? * to enter a synchronized block/method or

? ? * reenter a synchronized block/method after calling

? ? * {@link Object#wait() Object.wait}.

? ? */

? ? BLOCKED,? ? /**

? ? * Thread state for a waiting thread.

? ? * A thread is in the waiting state due to calling one of the

? ? * following methods:

? ? * <ul>

? ? *? <li>{@link Object#wait() Object.wait} with no timeout</li>

? ? *? <li>{@link #join() Thread.join} with no timeout</li>

? ? *? <li>{@link LockSupport#park() LockSupport.park}</li>

? ? * </ul>

? ? *

? ? * <p>A thread in the waiting state is waiting for another thread to

? ? * perform a particular action.

? ? *

? ? * For example, a thread that has called <tt>Object.wait()</tt>

? ? * on an object is waiting for another thread to call

? ? * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on

? ? * that object. A thread that has called <tt>Thread.join()</tt>

? ? * is waiting for a specified thread to terminate.

? ? */

? ? WAITING,? ? /**

? ? * Thread state for a waiting thread with a specified waiting time.

? ? * A thread is in the timed waiting state due to calling one of

? ? * the following methods with a specified positive waiting time:

? ? * <ul>

? ? *? <li>{@link #sleep Thread.sleep}</li>

? ? *? <li>{@link Object#wait(long) Object.wait} with timeout</li>

? ? *? <li>{@link #join(long) Thread.join} with timeout</li>

? ? *? <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>

? ? *? <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>

? ? * </ul>

? ? */

? ? TIMED_WAITING,? ? /**

? ? * Thread state for a terminated thread.

? ? * The thread has completed execution.

? ? */

? ? TERMINATED;

}

生命周期

線程的狀態(tài):NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。注釋也解釋得很清楚各個(gè)狀態(tài)的作用,而各個(gè)狀態(tài)的轉(zhuǎn)換也有一定的規(guī)則需要遵循的。

6.2動(dòng)作

介紹完線程的狀態(tài)和生命周期,接下來(lái)我了解的線程具備哪些常用的操作。首先線程也是一個(gè)普通的對(duì)象Thread,所有的線程都是Thread或者其子類的對(duì)象。那么這個(gè)內(nèi)存對(duì)象被創(chuàng)建出來(lái)后就會(huì)放在JVM的堆內(nèi)存空間,當(dāng)我們執(zhí)行start()方法的時(shí)候,對(duì)象的方法體在棧空間分配好對(duì)應(yīng)的棧幀來(lái)往執(zhí)行引擎輸送指令(也即是方法體翻譯成JVM的指令集)。

線程操作

新建線程:new? Thread(),新建一個(gè)線程對(duì)象,內(nèi)存為線程在棧上分配好內(nèi)存空間

啟動(dòng)線程:start(),告訴系統(tǒng)系統(tǒng)準(zhǔn)備就緒,只要資源允許隨時(shí)可以執(zhí)行我棧里面的指令了

執(zhí)行線程:run(),分配了CPU等計(jì)算資源,正在執(zhí)行棧里面的指令集

停止線程(過(guò)時(shí)):stop(),把CPU和內(nèi)存資源回收,線程消亡,由于太過(guò)粗暴,已經(jīng)被標(biāo)記為過(guò)時(shí)

線程中斷:

interrupt(),中斷是對(duì)線程打上了中斷標(biāo)簽,可供run()里面的方法體接收中斷信號(hào),至于線程要不要中斷,全靠業(yè)務(wù)邏輯設(shè)計(jì),而不是簡(jiǎn)單粗暴的把線程直接停掉

isInterrupt(),主要是run()方法體來(lái)判斷當(dāng)前線程是否被置為中斷

interrupted(),靜態(tài)方法,也是用戶判斷線程是否被置為中斷狀態(tài),同時(shí)判斷完將線程中斷狀態(tài)復(fù)位

線程休眠:sleep(),靜態(tài)方法,線程休眠指定時(shí)間段,此間讓出CPU資源給其他線程,但是線程依然持有對(duì)象鎖,其他線程無(wú)法進(jìn)入同步塊,休眠完成后也未必立刻執(zhí)行,需要等到資源允許才能執(zhí)行

線程等待(對(duì)象方法):wait(),是Object的方法,也即是對(duì)象的內(nèi)置方法,在同步塊中線程執(zhí)行到該方法時(shí),也即讓出了該對(duì)象的鎖,所以無(wú)法繼續(xù)執(zhí)行

線程通知(對(duì)象方法):notify(),notifyAll(),此時(shí)該對(duì)象持有一個(gè)或者多個(gè)線程的wait,調(diào)用notify()隨機(jī)的讓一個(gè)線程恢復(fù)對(duì)象的鎖,調(diào)用notifyAll()則讓所有線程恢復(fù)對(duì)象鎖

線程掛起(過(guò)時(shí)):suspend(),線程掛起并沒(méi)有釋放資源,而是只能等到resume()才能繼續(xù)執(zhí)行

線程恢復(fù)(過(guò)時(shí)):resume(),由于指令重排可能導(dǎo)致resume()先于suspend()執(zhí)行,導(dǎo)致線程永遠(yuǎn)掛起,所以該方法被標(biāo)為過(guò)時(shí)

線程加入:join(),在一個(gè)線程調(diào)用另外一個(gè)線程的join()方法表明當(dāng)前線程阻塞知道被調(diào)用線程執(zhí)行結(jié)束再進(jìn)行,也即是被調(diào)用線程織入進(jìn)來(lái)

線程讓步:yield(),暫停當(dāng)前線程進(jìn)而執(zhí)行別的線程,當(dāng)前線程等待下一輪資源允許再進(jìn)行,防止該線程一直霸占資源,而其他線程餓死

線程等待:park(),基于線程對(duì)象的操作,較對(duì)象鎖更為精準(zhǔn)

線程恢復(fù):unpark(Thread thread),對(duì)應(yīng)park()解鎖,為不可重入鎖

線程分組

為了管理線程,于是有了線程組的概念,業(yè)務(wù)上把類似的線程放在一個(gè)ThreadGroup里面統(tǒng)一管理。線程組表示一組線程,此外,線程組還可以包括其他線程組。線程組形成一個(gè)樹(shù),其中除了初始線程組以外的每個(gè)線程組都有一個(gè)父線程。線程被允許訪問(wèn)它自己的線程組信息,但不能訪問(wèn)線程組的父線程組或任何其他線程組的信息。

守護(hù)線程

通常情況下,線程運(yùn)行到最后一條指令后則完成生命周期,結(jié)束線程,然后系統(tǒng)回收資源。或者單遇到異常或者return提前返回,但是如果我們想讓線程常駐內(nèi)存的話,比如一些監(jiān)控類線程,需要24小時(shí)值班的,于是我們又創(chuàng)造了守護(hù)線程的概念。

setDaemon()傳入true則會(huì)把線程一直保持在內(nèi)存里面,除非JVM宕機(jī)否則不會(huì)退出。

線程優(yōu)先級(jí)

線程優(yōu)先級(jí)其實(shí)只是對(duì)線程打的一個(gè)標(biāo)志,但并不意味這高優(yōu)先級(jí)的一定比低優(yōu)先級(jí)的先執(zhí)行,具體還要看操作系統(tǒng)的資源調(diào)度情況。通常線程優(yōu)先級(jí)為5,邊界為[1,10]。

/**

? * The minimum priority that a thread can have.

? */

public final static int MIN_PRIORITY = 1;/**

? * The default priority that is assigned to a thread.

? */

public final static int NORM_PRIORITY = 5; /**

? * The maximum priority that a thread can have.

? */

public final static int MAX_PRIORITY = 10;

本節(jié)介紹了線程單元的轉(zhuǎn)態(tài)切換和常用的一些操作方法。如果只是單線程的話,其他都沒(méi)必要研究這些,重頭戲在于多線程直接的競(jìng)爭(zhēng)配合操作,下一節(jié)則重點(diǎn)介紹多個(gè)線程的交互需要關(guān)注哪些問(wèn)題。

7.線程交互

其實(shí)上一節(jié)介紹的線程狀態(tài)切換和線程操作都是為線程交互做準(zhǔn)備的。不然如果只是單線程完全沒(méi)必要搞什么通知,恢復(fù),讓步之類的操作了。

7.1交互方式

線程交互也就是線程直接的通信,最直接的辦法就是線程直接直接通信傳值,而間接方式則是通過(guò)共享變量來(lái)達(dá)到彼此的交互。

等待:釋放對(duì)象鎖,允許其他線程進(jìn)入同步塊

通知:重新獲取對(duì)象鎖,繼續(xù)執(zhí)行

中斷:狀態(tài)交互,通知其他線程進(jìn)入中斷

織入:合并線程,多個(gè)線程合并為一個(gè)

7.2線程安全

我們最關(guān)注的還是通過(guò)共享變量來(lái)達(dá)到交互的方式。線程如果都各自干活互不搭理的話自然相安無(wú)事,但多數(shù)情況下線程直接需要打交道,而且需要分享共享資源,那么這個(gè)時(shí)候最核心的就是線程安全了。

什么是線程安全?

當(dāng)多個(gè)線程訪問(wèn)同一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替運(yùn)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲取正確的結(jié)果,那這個(gè)對(duì)象是線程安全的。(摘自《深入Java虛擬機(jī)》)

如何保證線程安全?

我們最早接觸線程安全可能是JDK提供的一些號(hào)稱線程安全的容器,比如Vetor較ArrayList是線程安全,HashTable較HashMap是線程安全?其實(shí)線程安全類并不代表也不等同線程安全的程序,而線程不安全的類同樣可以完成線程安全的程序。我們關(guān)注的也就是寫出線程安全的程序,那么如何寫出線程安全的代碼呢?下面列舉了線程安全的主要設(shè)計(jì)技術(shù):

無(wú)狀態(tài)

這個(gè)有點(diǎn)函數(shù)式編程的味道,下文并發(fā)模式會(huì)介紹到,總之就是線程只有入?yún)⒑途植孔兞浚绻兞渴且玫脑挘_保變量的創(chuàng)建和調(diào)用生命周期都發(fā)生在線程棧內(nèi),就可以確保線程安全。

無(wú)共享狀態(tài)

完全要求線程無(wú)狀態(tài)比較難實(shí)現(xiàn),必要的狀態(tài)是無(wú)法避免的,那么我們就必須維護(hù)不同線程之間的不同狀態(tài),這可是個(gè)麻煩事。幸好我們有ThreadLocal這個(gè)神器,該對(duì)象跟當(dāng)前線程綁定,而且只對(duì)當(dāng)前線程可見(jiàn),完美解決了無(wú)共享狀態(tài)的問(wèn)題。

不可變狀態(tài)

最后實(shí)在沒(méi)辦法避免狀態(tài)共享,在線程之間共享狀態(tài),最怕的就是無(wú)法確保能維護(hù)好正確的讀寫順序,而且多線程確實(shí)也無(wú)法正確維護(hù)好這個(gè)共享變量。那么我們索性粗暴點(diǎn),把共享的狀態(tài)定位不可變,比如價(jià)格final修飾一下,這樣就達(dá)到安全狀態(tài)共享。

消息傳遞

一個(gè)線程通常也不是所有步驟都需要共享狀態(tài),而是部分環(huán)節(jié)才需要的,那么我們把共享狀態(tài)的代碼拆開(kāi),無(wú)共享狀態(tài)的那部分自然不用關(guān)心,而共享狀態(tài)的小段代碼,則通過(guò)加入消息組件來(lái)傳遞狀態(tài)。這個(gè)設(shè)計(jì)到并發(fā)模式的流水線編程模式,下文并發(fā)模式會(huì)重點(diǎn)介紹。

線程安全容器

JUC里面提供大量的并發(fā)容器,涉及到線程交互的時(shí)候,使用安全容器可以避免大部分的錯(cuò)誤,而且大大降低了代碼的復(fù)雜度。

通過(guò)synchronized給方法加上內(nèi)置鎖來(lái)實(shí)現(xiàn)線程安全的類如Vector,HashTable,StringBuffer

AtomicXXX如AtomicInteger

ConcurrentXXX如ConcurrentHashMap

BlockingQueue/BlockingDeque

CopyOnWriteArrayList/CopyOnWriteArraySet

ThreadPoolExecutor

synchronized同步

該關(guān)鍵字確保代碼塊同一時(shí)間只被一個(gè)線程執(zhí)行,在這個(gè)前提下再設(shè)計(jì)符合線程安全的邏輯

其作用域?yàn)?br>

對(duì)象:對(duì)象加鎖,進(jìn)入同步代碼塊之前獲取對(duì)象鎖

實(shí)例方法:對(duì)象加鎖,執(zhí)行實(shí)例方法前獲取對(duì)象實(shí)例鎖

類方法:類加鎖,執(zhí)行類方法前獲取類鎖

volatile約束

volatile確保每次操作都能強(qiáng)制同步CPU緩存和主存直接的變量。而且在編譯期間能阻止指令重排。讀寫并發(fā)情況下volatile也不能確保線程安全,上文解析內(nèi)存模型的時(shí)候有提到過(guò)。

這節(jié)我們論述了編寫線程安全程序的指導(dǎo)思想,其中我們提到了JDK提供的JUC工具包,下一節(jié)將重點(diǎn)介紹并發(fā)編程常用的趁手工具。

8.線程工具

前文我們介紹了內(nèi)存理論和線程的一些特征,大家都知道并發(fā)編程容易出錯(cuò),而且出了錯(cuò)還不好調(diào)試排查,幸好JDK里面集成了大量實(shí)用的API工具,我們能熟悉這些工具,寫起并發(fā)程序來(lái)也事半功倍。

工具篇其實(shí)就是對(duì)鎖的不斷變種,適應(yīng)更多的開(kāi)發(fā)場(chǎng)景,提高性能,提供更方便的工具,從最粗暴的同步修飾符,到靈活的可重入鎖,到寬松的條件,接著到允許多個(gè)線程訪問(wèn)的信號(hào)量,最后到讀寫分離鎖。

8.1同步控制

由于大多數(shù)的并發(fā)場(chǎng)景都是需要訪問(wèn)到共享資源的,為了保證線程安全,我們不得已采用鎖的技術(shù)來(lái)做同步控制,這節(jié)我們介紹的是適用不同場(chǎng)景各種鎖技術(shù)。

ReentrantLock

可重入互斥鎖具有與使用synchronized的隱式監(jiān)視器鎖具有相同的行為和語(yǔ)義,但具有更好擴(kuò)展功能。

ReentrantLock由最后成功鎖定的線程擁有,而且還未解鎖。當(dāng)鎖未被其他線程占有時(shí),線程調(diào)用lock()將返回并且成功獲取鎖。如果當(dāng)前線程已擁有鎖,則該方法將立即返回。這可以使用方法isHeldByCurrentThread()和getHoldCount()來(lái)檢查。

構(gòu)造函數(shù)接受可選的fairness參數(shù)。當(dāng)設(shè)置為true時(shí),在競(jìng)爭(zhēng)條件下,鎖定有利于賦予等待時(shí)間最長(zhǎng)線程的訪問(wèn)權(quán)限。否則,鎖將不保證特定的訪問(wèn)順序。在多線程訪問(wèn)的情況,使用公平鎖比默認(rèn)設(shè)置,有著更低的吞吐量,但是獲得鎖的時(shí)間比較小而且可以避免等待鎖導(dǎo)致的饑餓。但是,鎖的公平性并不能保證線程調(diào)度的公平性。因此,使用公平鎖的許多線程中的一個(gè)可以連續(xù)多次獲得它,而其他活動(dòng)線程沒(méi)有進(jìn)展并且當(dāng)前沒(méi)有持有鎖。不定時(shí)的tryLock()方法不遵循公平性設(shè)置。即使其他線程正在等待,如果鎖可用,它也會(huì)成功。

任意指定鎖的起始位置

中斷響應(yīng)

鎖申請(qǐng)等待限時(shí)tryLock()

公平鎖

Condition

Condition從擁有監(jiān)控方法(wait,notify,notifyAll)的Object對(duì)象中抽離出來(lái)成為獨(dú)特的對(duì)象,高效的讓每個(gè)對(duì)象擁有更多的等待線程。和鎖對(duì)比起來(lái),如果說(shuō)用Lock代替synchronized,那么Condition就是用來(lái)代替Object本身的監(jiān)控方法。

Condition實(shí)例跟Object本身的監(jiān)控相似,同樣提供wait()方法讓調(diào)用的線程暫時(shí)掛起讓出資源,知道其他線程通知該對(duì)象轉(zhuǎn)態(tài)變化,才可能繼續(xù)執(zhí)行。Condition實(shí)例來(lái)源于Lock實(shí)例,通過(guò)Lock調(diào)用newCondition()即可。Condition較Object原生監(jiān)控方法,可以保證通知順序。

Semaphore

鎖和同步塊同時(shí)只能允許單個(gè)線程訪問(wèn)共享資源,這個(gè)明顯有些單調(diào),部分場(chǎng)景其實(shí)可以允許多個(gè)線程訪問(wèn),這個(gè)時(shí)候信號(hào)量實(shí)例就派上用場(chǎng)了。信號(hào)量邏輯上維持了一組許可證, 線程調(diào)用acquire()阻塞直到許可證可用后才能執(zhí)行。 執(zhí)行release()意味著釋放許可證,實(shí)際上信號(hào)量并沒(méi)有真正的許可證,只是采用了計(jì)數(shù)功能來(lái)實(shí)現(xiàn)這個(gè)功能。

ReadWriteLock

顧名思義讀寫鎖將讀寫分離,細(xì)化了鎖的粒度,照顧到性能的優(yōu)化。

CountDownLatch

這個(gè)鎖有點(diǎn)“關(guān)門放狗”的意思,尤其在我們壓測(cè)的時(shí)候模擬實(shí)時(shí)并行請(qǐng)求,該實(shí)例將線程積累到指定數(shù)量后,調(diào)用countDown()方法讓所有線程同時(shí)執(zhí)行。

CyclicBarrier

CyclicBarrier是加強(qiáng)版的CountDownLatch,上面講的是一次性“關(guān)門放狗”,而循環(huán)柵欄則是集齊了指定數(shù)量的線程,在資源都允許的情況下同時(shí)執(zhí)行,然后下一批同樣的操作,周而復(fù)始。

LockSupport

LockSupport是用來(lái)創(chuàng)建鎖和其他同步類的基本線程阻塞原語(yǔ)。 LockSupport中的park() 和 unpark() 的作用分別是阻塞線程和解除阻塞線程,而且park()和unpark()不會(huì)遇到“Thread.suspend 和 Thread.resume所可能引發(fā)的死鎖”問(wèn)題。因?yàn)閜ark() 和 unpark()有許可的存在;調(diào)用 park() 的線程和另一個(gè)試圖將其 unpark() 的線程之間的競(jìng)爭(zhēng)將保持活性。

8.2線程池

線程池總覽

線程多起來(lái)的話就需要管理,不然就會(huì)亂成一鍋。我們知道線程在物理上對(duì)應(yīng)的就是棧里面的一段內(nèi)存,存放著局部變量的空間和待執(zhí)行指令集。如果每次執(zhí)行都要從頭初始化這段內(nèi)存,然后再交給CPU執(zhí)行,效率就有點(diǎn)低了。假如我們知道該段棧內(nèi)存會(huì)被經(jīng)常用到,那我們就不要回收,創(chuàng)建完就讓它在棧里面呆著,要用的時(shí)候取出來(lái),用完換回去,是不是就省了初始化線程空間的時(shí)間,這樣是我們搞出線程池的初衷。

其實(shí)線程池很簡(jiǎn)單,就是搞了個(gè)池子放了一堆線程。既然我們搞線程池是為了提高效率,那就要考慮線程池放多少個(gè)線程比較合適,太多了或者太少了有什么問(wèn)題,怎么拒絕多余的請(qǐng)求,除了異常怎么處理。首先我們來(lái)看跟線程池有關(guān)的一張類圖。

線程池歸結(jié)起來(lái)就是這幾個(gè)類的使用技巧了,重點(diǎn)關(guān)注ThreadPoolExecutor和Executors即可。

創(chuàng)建線程池

萬(wàn)變不離其宗,創(chuàng)建線程池的各種馬甲方法最后都是調(diào)用到這方法里面,包含核心線程數(shù),最大線程數(shù),線程工廠,拒絕策略等參數(shù)。其中線程工廠則可以實(shí)現(xiàn)自定義創(chuàng)建線程的邏輯。

public interface ThreadFactory {? ? Thread newThread(Runnable r);

}

創(chuàng)建的核心構(gòu)造方法ThreadPoolExecutor.java? 1301

/**

? ? * Creates a new {@code ThreadPoolExecutor} with the given initial

? ? * parameters.

? ? *

? ? * @param corePoolSize the number of threads to keep in the pool, even

? ? *? ? ? ? if they are idle, unless {@code allowCoreThreadTimeOut} is set

? ? * @param maximumPoolSize the maximum number of threads to allow in the

? ? *? ? ? ? pool

? ? * @param keepAliveTime when the number of threads is greater than

? ? *? ? ? ? the core, this is the maximum time that excess idle threads

? ? *? ? ? ? will wait for new tasks before terminating.

? ? * @param unit the time unit for the {@code keepAliveTime} argument

? ? * @param workQueue the queue to use for holding tasks before they are

? ? *? ? ? ? executed.? This queue will hold only the {@code Runnable}

? ? *? ? ? ? tasks submitted by the {@code execute} method.

? ? * @param threadFactory the factory to use when the executor

? ? *? ? ? ? creates a new thread

? ? * @param handler the handler to use when execution is blocked

? ? *? ? ? ? because the thread bounds and queue capacities are reached

? ? * @throws IllegalArgumentException if one of the following holds:<br>

? ? *? ? ? ? {@code corePoolSize < 0}<br>

? ? *? ? ? ? {@code keepAliveTime < 0}<br>

? ? *? ? ? ? {@code maximumPoolSize <= 0}<br>

? ? *? ? ? ? {@code maximumPoolSize < corePoolSize}

? ? * @throws NullPointerException if {@code workQueue}

? ? *? ? ? ? or {@code threadFactory} or {@code handler} is null

? ? */

? ? public ThreadPoolExecutor(int corePoolSize,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? int maximumPoolSize,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? long keepAliveTime,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? TimeUnit unit,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? BlockingQueue<Runnable> workQueue,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ThreadFactory threadFactory,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? RejectedExecutionHandler handler)

拒絕策略包含:

? ? /** 實(shí)際上并未真正丟棄任務(wù),但是線程池性能會(huì)下降

? ? * A handler for rejected tasks that runs the rejected task

? ? * directly in the calling thread of the {@code execute} method,

? ? * unless the executor has been shut down, in which case the task

? ? * is discarded.

? ? */

? ? public static class CallerRunsPolicy implements RejectedExecutionHandler

? ? /** 粗暴停止拋異常

? ? * A handler for rejected tasks that throws a

? ? * {@code RejectedExecutionException}.

? ? */? ? public static class AbortPolicy implements RejectedExecutionHandler

? ? /** 悄無(wú)聲息的丟棄拒絕的任務(wù)

? ? * A handler for rejected tasks that silently discards the

? ? * rejected task.

? ? */? ? public static class DiscardPolicy implements RejectedExecutionHandler

? ? /** 丟棄最老的請(qǐng)求

? ? * A handler for rejected tasks that discards the oldest unhandled

? ? * request and then retries {@code execute}, unless the executor

? ? * is shut down, in which case the task is discarded.

? ? */? ? public static class DiscardOldestPolicy implements RejectedExecutionHandler

包括Executors.java中的創(chuàng)建線程池的方法,具體實(shí)現(xiàn)也是通過(guò)ThreadPoolExecutor來(lái)創(chuàng)建的。

public static ExecutorService newCachedThreadPool() {

? ? return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 60L, TimeUnit.SECONDS,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? new SynchronousQueue<Runnable>());

}

public static ExecutorService newFixedThreadPool(int nThreads) {

? ? return new ThreadPoolExecutor(nThreads, nThreads,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0L, TimeUnit.MILLISECONDS,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? new LinkedBlockingQueue<Runnable>());

}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {

? ? return new ScheduledThreadPoolExecutor(corePoolSize);

}

public static ExecutorService newSingleThreadExecutor() {

? ? return new FinalizableDelegatedExecutorService

? ? ? ? ? ? (new ThreadPoolExecutor(1, 1,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0L, TimeUnit.MILLISECONDS,

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? new LinkedBlockingQueue<Runnable>()));

}

調(diào)用線程池

ThreadPoolExecutor.java 1342

/** 同步執(zhí)行線程,出現(xiàn)異常打印堆棧信息

* Executes the given task sometime in the future.? The task

* may execute in a new thread or in an existing pooled thread.

*

* If the task cannot be submitted for execution, either because this

* executor has been shutdown or because its capacity has been reached,

* the task is handled by the current {@code RejectedExecutionHandler}.

*

* @param command the task to execute

* @throws RejectedExecutionException at discretion of

*? ? ? ? {@code RejectedExecutionHandler}, if the task

*? ? ? ? cannot be accepted for execution

* @throws NullPointerException if {@code command} is null

*/public void execute(Runnable command)/**

* 異步提交線程任務(wù),出現(xiàn)異常無(wú)法同步追蹤堆棧,本質(zhì)上也是調(diào)用execute()方法

*/public <T> Future<T> submit(Runnable task, T result) {? ? if (task == null) throw new NullPointerException();

? ? RunnableFuture<T> ftask = newTaskFor(task, result);

? ? execute(ftask);? ? return ftask;

}

線程池優(yōu)化

線程池已經(jīng)是我們使用線程的一個(gè)優(yōu)化成果了,而線程池本身的優(yōu)化其實(shí)就是根據(jù)實(shí)際業(yè)務(wù)選擇好不同類型的線程池,預(yù)估并發(fā)線程數(shù)量,控制好線程池預(yù)留線程數(shù)(最大線程數(shù)一般設(shè)為2N+1最好,N是CPU核數(shù)),這些涉及CPU數(shù)量,核數(shù)還有具體業(yè)務(wù)。

另外我們還注意到ForkJoinPool繼承了AbstractExecutorService,這是在JDK7才加上去的,目的就是提高任務(wù)派生出來(lái)更多任務(wù)的執(zhí)行效率,由上圖的繼承關(guān)系我們可以知道跟普通線程池最大的差異是執(zhí)行的任務(wù)類型不同。

public void execute(ForkJoinTask<?> task) {? ? if (task == null)? ? ? ? throw new NullPointerException();

? ? externalPush(task);

}public void execute(Runnable task) {? ? ? ? if (task == null)? ? ? ? ? ? throw new NullPointerException();

? ? ? ? ForkJoinTask<?> job;? ? ? ? if (task instanceof ForkJoinTask<?>) // avoid re-wrap

? ? ? ? ? ? job = (ForkJoinTask<?>) task;? ? ? ? else

? ? ? ? ? ? job = new ForkJoinTask.RunnableExecuteAction(task);

? ? ? ? externalPush(job);

}

8.3并發(fā)容器

其實(shí)我們?nèi)粘i_(kāi)發(fā)大多數(shù)并發(fā)場(chǎng)景直接用JDK 提供的線程安全數(shù)據(jù)結(jié)構(gòu)足矣,下面列舉了常用的列表,集合等容器,具體就不展開(kāi)講,相信大家都用得很熟悉了。

ConcurrentHashMap

CopyOnWriteArrayList

ConcurrentLinkedQueue

BlockingQueue

ConcurrentSkipListMap

Vector

HashTable


9.線程調(diào)優(yōu)

9.1性能指標(biāo)

回想一下,當(dāng)我們?cè)谡勑阅軆?yōu)化的時(shí)候,我們可能指的是數(shù)據(jù)庫(kù)的讀寫次數(shù),也可能指網(wǎng)站的響應(yīng)時(shí)間。通常我們會(huì)用QPS,TPS,RT,并發(fā)數(shù),吞吐量,更進(jìn)一步的還會(huì)對(duì)比CPU負(fù)載來(lái)衡量一個(gè)系統(tǒng)的性能。

當(dāng)然我們知道一個(gè)系統(tǒng)的吞吐量和響應(yīng)時(shí)間跟外部網(wǎng)絡(luò),分布式架構(gòu)等都存在強(qiáng)關(guān)聯(lián),性能優(yōu)化也跟各級(jí)緩存設(shè)計(jì),數(shù)據(jù)冗余等架構(gòu)有很大關(guān)系,假設(shè)其他方面我們都已經(jīng)完成了,聚焦到本文我們暫時(shí)關(guān)心的是單節(jié)點(diǎn)的性能優(yōu)化。畢竟一屋不掃何以掃天下,整體系統(tǒng)的優(yōu)化也有賴于各個(gè)節(jié)點(diǎn)的調(diào)優(yōu)。從感官上來(lái)談,當(dāng)請(qǐng)求量很少的時(shí)候,我們可以很輕松的通過(guò)各種緩存優(yōu)化來(lái)提高響應(yīng)時(shí)間。但是隨著用戶激增,請(qǐng)求次數(shù)的增加,我們的服務(wù)也對(duì)應(yīng)著需要并發(fā)模型來(lái)支撐。但是一個(gè)節(jié)點(diǎn)的并發(fā)量有個(gè)上限,當(dāng)達(dá)到這個(gè)上限后,響應(yīng)時(shí)間就會(huì)變長(zhǎng),所以我們需要探索并發(fā)到什么程度才是最優(yōu)的,才能保證最高的并發(fā)數(shù),同時(shí)響應(yīng)時(shí)間又能保持在理想情況。由于我們暫時(shí)不關(guān)注節(jié)點(diǎn)以外的網(wǎng)絡(luò)情況,那么下文我們特指的RT是指服務(wù)接收到請(qǐng)求后,完成計(jì)算,返回計(jì)算結(jié)果經(jīng)歷的時(shí)間。

單線程

單線程情況下,服務(wù)接收到請(qǐng)求后開(kāi)始初始化,資源準(zhǔn)備,計(jì)算,返回結(jié)果,時(shí)間主要花在CPU計(jì)算和CPU外的IO等待時(shí)間,多個(gè)請(qǐng)求來(lái)也只能排隊(duì)一個(gè)一個(gè)來(lái),那么RT計(jì)算如下

RT = T(cpu) + T(io)

QPS = 1000ms / RT

多線程

單線程情況很好計(jì)算,多線程情況就復(fù)雜了,我們目標(biāo)是計(jì)算出最佳并發(fā)量,也就是線程數(shù)N

單核情況:N = [T(cpu) + T(io)] / T(cpu)

M核情況:N = [T(cpu) + T(io)] / T(cpu) * M

由于多核情況CPU未必能全部使用,存在一個(gè)資源利用百分比P

那么并發(fā)的最佳線程數(shù) N = [T(cpu) + T(io)] / T(cpu) M P

吞吐量

我們知道單線程的QPS很容易算出來(lái),那么多線程的QPS

QPS = 1000ms / RT N = 1000ms / T(cpu) + T(io) [T(cpu) + T(io)] / T(cpu) M P= 1000ms / T(cpu) M P

在機(jī)器核數(shù)固定情況下,也即是并發(fā)模式下最大的吞吐量跟服務(wù)的CPU處理時(shí)間和CPU利用率有關(guān)。CPU利用率不高,就是通常我們聽(tīng)到最多的抱怨,壓測(cè)時(shí)候qps都打滿了,但是cpu的load就是上不去。并發(fā)模型中多半個(gè)共享資源有關(guān),而共享資源又跟鎖息息相關(guān),那么大部分時(shí)候我們想對(duì)節(jié)點(diǎn)服務(wù)做性能調(diào)優(yōu)時(shí)就是對(duì)鎖的優(yōu)化,這個(gè)下一節(jié)會(huì)提到。

前面我們是假設(shè)機(jī)器核數(shù)固定的情況下做優(yōu)化的,那假如我們把緩存,IO,鎖都優(yōu)化了,剩下的還有啥空間去突破呢?回想一下我們談基礎(chǔ)理論的時(shí)候提到的Amdahl定律,公式之前已經(jīng)給出,該定律想表達(dá)的結(jié)論是隨著核數(shù)或者處理器個(gè)數(shù)的增加,可以增加優(yōu)化加速比,但是會(huì)達(dá)到上限,而且增加趨勢(shì)愈發(fā)不明顯。

9.2鎖優(yōu)化

說(shuō)真的,我們并不喜歡鎖的,只不過(guò)由于臨界資源的存在不得已為之。如果業(yè)務(wù)上設(shè)計(jì)能避免出現(xiàn)臨界資源,那就沒(méi)有鎖優(yōu)化什么事了。但是,鎖優(yōu)化的一些原則還是要說(shuō)一說(shuō)的。

時(shí)間

既然我們并不喜歡鎖,那么就按需索取,只在核心的同步塊加鎖,用完立馬釋放,減少鎖定臨界區(qū)的時(shí)間,這樣就可以把資源競(jìng)爭(zhēng)的風(fēng)險(xiǎn)降到最低。

粒度

進(jìn)一步看,有時(shí)候我們核心同步塊可以進(jìn)一步分離,比如只讀的情況下并不需要加鎖,這時(shí)候就可以用讀寫鎖各自的讀寫功能。

還有一種情況,有時(shí)候我們反而會(huì)小心翼翼的到處加鎖來(lái)防止意外出現(xiàn),可能出現(xiàn)三個(gè)同步塊加了三個(gè)鎖,這也造成CPU的過(guò)多停頓,根據(jù)業(yè)務(wù)其實(shí)可以把相關(guān)邏輯合并起來(lái),也就是鎖粗化。

鎖的分離和粗化具體還得看業(yè)務(wù)如何操作。

尺度

除了鎖暫用時(shí)間和粒度外,還有就是鎖的尺度,還是根據(jù)業(yè)務(wù)來(lái),能用共享鎖定的情況就不要用獨(dú)享鎖。

死鎖

這個(gè)不用說(shuō)都知道,死鎖防不勝防,我們前面也介紹很多現(xiàn)成的工具,比如可重入鎖,還有線程本地變量等方式,都可以一定程度避免死鎖。

9.3JVM鎖機(jī)制

我們?cè)诖a層面把鎖的應(yīng)用都按照安全法則做到最好了,那接下來(lái)要做的就是下鉆到JVM級(jí)別的鎖優(yōu)化。具體實(shí)現(xiàn)原理我們暫不展開(kāi),后續(xù)有機(jī)會(huì)再搞個(gè)專題寫寫JVM鎖實(shí)現(xiàn)。

自旋鎖(Spin Lock)

自旋鎖的原理非常簡(jiǎn)單。如果持有鎖的線程可以在短時(shí)間內(nèi)釋放鎖資源,那么等待競(jìng)爭(zhēng)鎖的那些線程不需要在內(nèi)核狀態(tài)和用戶狀態(tài)之間進(jìn)行切換。 它只需要等待,并且鎖可以在釋放鎖之后立即獲得鎖。這可以避免消耗用戶線程和內(nèi)核切換。

但是,自旋鎖讓CPU空等著什么也不干也是一種浪費(fèi)。 如果自旋鎖的對(duì)象一直無(wú)法獲得臨界資源,則線程也無(wú)法在沒(méi)有執(zhí)行實(shí)際計(jì)算的情況下一致進(jìn)行CPU空轉(zhuǎn),因此需要設(shè)置自旋鎖的最大等待時(shí)間。如果持有鎖的線程在旋轉(zhuǎn)等待的最大時(shí)間沒(méi)有釋放鎖,則自旋鎖線程將停止旋轉(zhuǎn)進(jìn)入阻塞狀態(tài)。

JDK1.6開(kāi)啟自旋鎖? -XX:+UseSpinning,1.7之后控制器收回到JVM自主控制

偏向鎖(Biased Lock)

偏向鎖偏向于第一個(gè)訪問(wèn)鎖的線程,如果在運(yùn)行過(guò)程中,同步鎖只有一個(gè)線程訪問(wèn),不存在多線程爭(zhēng)用的情況,則線程是不需要觸發(fā)同步的,這種情況下,就會(huì)給線程加一個(gè)偏向鎖。如果在運(yùn)行過(guò)程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會(huì)被掛起,JVM會(huì)消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級(jí)鎖。

JDK1.6開(kāi)啟自旋鎖? -XX:+UseBiasedLocking,1.7之后控制器收回到JVM自主控制

輕量級(jí)鎖(Lightweight Lock)

輕量級(jí)鎖是由偏向鎖升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€(gè)線程加入鎖競(jìng)爭(zhēng)的時(shí)候,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。

重量級(jí)鎖(Heavyweight Lock)

如果鎖檢測(cè)到與另一個(gè)線程的爭(zhēng)用,則鎖定會(huì)膨脹至重量級(jí)鎖。也就是我們常規(guī)用的同步修飾產(chǎn)生的同步作用。

9.4無(wú)鎖

最后其實(shí)我想說(shuō)的是,雖然鎖很符合我們?nèi)祟惖倪壿嬎季S,設(shè)計(jì)起來(lái)也相對(duì)簡(jiǎn)單,但是擺脫不了臨界區(qū)的限制。那么我們不妨換個(gè)思路,進(jìn)入無(wú)鎖的時(shí)間,也就是我們可能會(huì)增加業(yè)務(wù)復(fù)雜度的情況下,來(lái)消除鎖的存在。

CAS策略

著名的CAS(Compare And Swap),是多線程中用于實(shí)現(xiàn)同步的原子指令。 它將內(nèi)存位置的內(nèi)容與給定值進(jìn)行比較,并且只有它們相同時(shí),才將該內(nèi)存位置的內(nèi)容修改為新的給定值。 這是作為單個(gè)原子操作完成的。 原子性保證了新值是根據(jù)最新信息計(jì)算出來(lái)的; 如果在此期間該值已被另一個(gè)線程更新,則寫入將失敗。 操作的結(jié)果必須表明它是否進(jìn)行了替換; 這可以通過(guò)簡(jiǎn)單的Boolean來(lái)響應(yīng),或通過(guò)返回從內(nèi)存位置讀取的值(而不是寫入它的值)來(lái)完成。

也就是一個(gè)原子操作包含了要操作的數(shù)據(jù)和給定認(rèn)為正確的值進(jìn)行對(duì)比,一致的話就繼續(xù),不一致則會(huì)重試。這樣就在沒(méi)有鎖的情況下完成并發(fā)操作。

我們知道原子類 AtomicInteger內(nèi)部實(shí)現(xiàn)的原理就是采用了CAS策略來(lái)完成的。

AtomicInteger.java? 132

/**

* Atomically sets the value to the given updated value

* if the current value {@code ==} the expected value.

*

* @param expect the expected value

* @param update the new value

* @return {@code true} if successful. False return indicates that

* the actual value was not equal to the expected value.

*/public final boolean compareAndSet(int expect, int update) {? ? return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

類似的還有AtomicReference.java? 115

/**

* Atomically sets the value to the given updated value

* if the current value {@code ==} the expected value.

* @param expect the expected value

* @param update the new value

* @return {@code true} if successful. False return indicates that

* the actual value was not equal to the expected value.

*/public final boolean compareAndSet(V expect, V update) {? ? return unsafe.compareAndSwapObject(this, valueOffset, expect, update);

}

有興趣的同學(xué)可以再了解一下Unsafe的實(shí)現(xiàn),進(jìn)一步可以了解Distuptor無(wú)鎖框架。

10.并發(fā)模型

前面我們大費(fèi)周章的從并發(fā)的基礎(chǔ)概念到多線程的使用方法和優(yōu)化技巧。但都是戰(zhàn)術(shù)層面的,本節(jié)我們?cè)囍鴱膽?zhàn)略的高度來(lái)擴(kuò)展一下并發(fā)編程的世界。可能大多數(shù)情況下我們談并發(fā)都會(huì)想到多線程,但是本節(jié)我們要打破這種思維,在完全不用搞多線程那一套的情況下實(shí)現(xiàn)并發(fā)。

首先我們用”多線程模式“來(lái)回顧前文所講的所有關(guān)于Thread衍生出來(lái)的定義,開(kāi)發(fā)和優(yōu)化的技術(shù)。

多線程模式

單位線程完成完整的任務(wù),也即是一條龍服務(wù)線程。

優(yōu)勢(shì):

映射現(xiàn)實(shí)單一任務(wù),便于理解和編碼

劣勢(shì):

有狀態(tài)多線程共享資源,導(dǎo)致資源競(jìng)爭(zhēng),死鎖問(wèn)題,線程等待阻塞,失去并發(fā)意義

有狀態(tài)多線程非阻塞算法,有利減少競(jìng)爭(zhēng),提升性能,但難以實(shí)現(xiàn)

多線程執(zhí)行順序無(wú)法預(yù)知

流水線模型

介紹完傳統(tǒng)多線程工作模式后,我們來(lái)學(xué)習(xí)另外一種并發(fā)模式,傳統(tǒng)的多線程工作模式,理解起來(lái)很直觀,接下來(lái)我們要介紹的另外一種并發(fā)模式看起來(lái)就不那么直觀了。

流水線模型,特點(diǎn)是無(wú)狀態(tài)線程,無(wú)狀態(tài)也意味著無(wú)需競(jìng)爭(zhēng)共享資源,無(wú)需等待,也就是非阻塞模型。流水線模型顧名思義就是流水線上有多個(gè)環(huán)節(jié),每個(gè)環(huán)節(jié)完成自己的工作后就交給下一個(gè)環(huán)節(jié),無(wú)需等待上游,周而復(fù)始的完成自己崗位上的一畝三分地就行。各個(gè)環(huán)節(jié)之間交付無(wú)需等待,完成即可交付。

而工廠的流水線也不止一條,所以有多條流水線同時(shí)工作。

不同崗位的生產(chǎn)效率是不一樣的,所以不同流水線之間也可以發(fā)生協(xié)同。

我們說(shuō)流水線模型也稱為響應(yīng)式模型或者事件驅(qū)動(dòng)模型,其實(shí)就是流水線上上游崗位完成生產(chǎn)就通知下游崗位,所以完成了一個(gè)事件的通知,每完成一次就通知一下,就是響應(yīng)式的意思。

流水線模型總體的思想就是縱向切分任務(wù),把任務(wù)里面耗時(shí)過(guò)久的環(huán)節(jié)單獨(dú)隔離出來(lái),避免完成一個(gè)任務(wù)需要耗費(fèi)等待的時(shí)間。在實(shí)現(xiàn)上又分為Actors和Channels模型

Actors

該模型跟我們講述的流水線模型基本一致,可以理解為響應(yīng)式模型

Channels

由于各個(gè)環(huán)節(jié)直接不直接交互,所以上下游之間并不知道對(duì)方是誰(shuí),好比不同環(huán)節(jié)直接用的是幾條公共的傳送帶來(lái)接收物品,各自只需要把完成后的半成品扔到傳送帶,即使后面流水線優(yōu)化了,去掉中間的環(huán)節(jié),對(duì)于個(gè)體崗位來(lái)說(shuō)也是無(wú)感知的,它只是周而復(fù)始的從傳送帶拿物品來(lái)加工。

流水線的優(yōu)缺點(diǎn):

優(yōu)勢(shì):

無(wú)共享狀態(tài):無(wú)需考慮資源搶占,死鎖等問(wèn)題

獨(dú)享內(nèi)存:worker可以持有內(nèi)存,合并多次操作到內(nèi)存后再持久化,提升效率

貼合底層:?jiǎn)尉€程模式貼合硬件運(yùn)行流程,便于代碼維護(hù)

任務(wù)順序可預(yù)知

劣勢(shì):

不夠直觀:一個(gè)任務(wù)被拆分為流水線上多個(gè)環(huán)節(jié),代碼層面難以直觀理解業(yè)務(wù)邏輯

由于流水線模式跟人類的順序執(zhí)行思維不一樣,比較費(fèi)解,那么有沒(méi)有辦法讓我們編碼的時(shí)候像寫傳統(tǒng)的多線程代碼一樣,而運(yùn)行起來(lái)又是流水線模式呢?答案是肯定的,比如基于Java的Akka/Reator/Vert.x/Play/Qbit框架,或者golang就是為流水線模式而生的并發(fā)語(yǔ)言,還有nodeJS等等。

流水線模型的開(kāi)發(fā)實(shí)踐可以參考流水線模型實(shí)踐。

其實(shí)流水線模型背后用的也還是多線程來(lái)實(shí)現(xiàn),只不過(guò)對(duì)于傳統(tǒng)多線程模式下我們需要小心翼翼來(lái)處理跟蹤資源共享問(wèn)題,而流水線模式把以前一個(gè)線程做的事情拆成多個(gè),每一個(gè)環(huán)節(jié)再用一條線程來(lái)完成,避免共享,線程直接通過(guò)管道傳輸消息。

這一塊展開(kāi)也是一個(gè)專題,主要設(shè)計(jì)NIO,Netty和Akka的編程實(shí)踐,先占坑后面補(bǔ)上。

函數(shù)式模型

函數(shù)式并行模型類似流水線模型,單一的函數(shù)是無(wú)狀態(tài)的,所以避免了資源競(jìng)爭(zhēng)的復(fù)雜度,同時(shí)每個(gè)函數(shù)類似流水線里面的單一環(huán)境,彼此直接通過(guò)函數(shù)調(diào)用傳遞參數(shù)副本,函數(shù)之外的數(shù)據(jù)不會(huì)被修改。函數(shù)式模式跟流水線模式相輔相成逐漸成為更為主流的并發(fā)架構(gòu)。具體的思想和編程實(shí)踐也是個(gè)大專題,篇幅限制本文就先不展開(kāi),擬在下個(gè)專題中詳細(xì)介紹《函數(shù)式編程演化》。

11.總結(jié)

由于CPU和I/O天然存在的矛盾,傳統(tǒng)順序的同步工作模式導(dǎo)致任務(wù)阻塞,CPU空等著沒(méi)有執(zhí)行,浪費(fèi)資源。多線程為突破了同步工作模式的情況下浪費(fèi)CPU資源,即使單核情況下也能將時(shí)間片拆分成單位給更多的線程來(lái)輪詢享用。多線程在不同享狀態(tài)的情況下非常高效,不管協(xié)同式還是搶占式都能在單位時(shí)間內(nèi)執(zhí)行更多的任務(wù),從而更好的榨取CPU資源。

但是多數(shù)情況下線程之間是需要通信的,這一核心場(chǎng)景導(dǎo)致了一系列的問(wèn)題,也就是線程安全。內(nèi)存被共享的單位由于被不同線程輪番讀取寫入操作,這種操作帶來(lái)的后果往往是寫代碼的人類沒(méi)想到的,也就是并發(fā)帶來(lái)的臟數(shù)據(jù)等問(wèn)題。解決了資源使用效率問(wèn)題,又帶來(lái)了新的安全問(wèn)題,如何解決?悲觀方式就是對(duì)于存在共享內(nèi)存的場(chǎng)景,無(wú)論如何只同意同一時(shí)刻一個(gè)線程操作,也就是同步操作方法或者代碼段或者顯示加鎖。或者volatile來(lái)使共享的主存跟每條線程的工作內(nèi)存同步(每次讀都從主存刷新,每次寫完都刷到主存)

要保證線程安全:

1、不要使用多線程,

2、多線程各干各的不要共享內(nèi)存,

3、共享的內(nèi)存空間是不可變的(常量,final),

4、實(shí)在要變每次變完要同步到主存volatile(依賴當(dāng)前值的邏輯除外),

5、原子變量,

6、根據(jù)具體業(yè)務(wù),避免臟數(shù)據(jù)(這塊就是多線程最容易犯錯(cuò)的地方)

線程安全后,要考慮的就是效率問(wèn)題,如果不解決效率問(wèn)題,那還干嘛要多線程。。。

如果所有線程都很自覺(jué),快速執(zhí)行完就跑路,那就是我們的理想情況了。但是,部分線程又臭又長(zhǎng)(I/O阻塞),不能讓一直賴在CPU不走,就把他上下文(線程號(hào),變量,執(zhí)行到哪等數(shù)值的快照)保存到內(nèi)存,然后讓它滾蛋下一個(gè)線程來(lái)。但是切換太快的話也不合適,畢竟每次保存線程的作案現(xiàn)場(chǎng)也要花不少時(shí)間的,單位時(shí)間執(zhí)行線程數(shù)要控制在一個(gè)適當(dāng)?shù)膫€(gè)數(shù)。創(chuàng)建線程也是一項(xiàng)很吃力的工作,一個(gè)線程就是在棧內(nèi)存里面開(kāi)辟一段內(nèi)存空間,根據(jù)字節(jié)碼分配臨時(shí)變量空間,不同操作系統(tǒng)通常不一樣。不能頻繁的創(chuàng)建銷毀線程。那就搞個(gè)線程池出來(lái),用的時(shí)候拿出來(lái),用完扔回去,簡(jiǎn)單省事。但是線程池的創(chuàng)建也有門道,不能無(wú)限創(chuàng)建不然就失去意義了。操作系統(tǒng)有一定上限,線程池太多線程內(nèi)存爆了,系統(tǒng)奔潰,所以需要一個(gè)機(jī)制。容納1024個(gè)線程,多了排隊(duì)再多了扔掉。回到線程切換,由于創(chuàng)建線程耗費(fèi)資源,切換也花費(fèi),有時(shí)候切換線程的時(shí)間甚至比讓線程待在cpu無(wú)所事事更長(zhǎng),那就給加個(gè)自旋鎖,就是讓它自己再cpu打滾啥事不干,一會(huì)兒輪到它里面就能干活。

既然多線程同步又得加鎖耗資源,不同步又有共享安全問(wèn)題。那能不能把這些鎖,共享,同步,要注意的問(wèn)題封裝起來(lái)。搞出一個(gè)異步的工作機(jī)制,不用管底層的同步問(wèn)題,只管業(yè)務(wù)問(wèn)題。傳統(tǒng)是工匠干活一根筋干完,事件驅(qū)動(dòng)是流水線,把一件事拆分成多個(gè)環(huán)節(jié),每個(gè)環(huán)節(jié)有唯一標(biāo)識(shí),各個(gè)環(huán)節(jié)批量生產(chǎn),在流水線對(duì)接。這樣在CPU單獨(dú)干,不共享,不阻塞,干完自己的通知管工,高效封裝了內(nèi)部線程的運(yùn)行規(guī)則,把業(yè)務(wù)關(guān)系暴露給管理者。

本文主要將的數(shù)基于JAVA的傳統(tǒng)多線程并發(fā)模型,下面例牌給出知識(shí)體系圖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。