1.概述
軟件業(yè)發(fā)展初期,程序編寫都是以算法為核心,程序員會把數(shù)據(jù)和過程分別作為獨立的部分來考慮,數(shù)據(jù)代表問題空間中的客體,程序代碼則用于處理這些數(shù)據(jù),這種思維方式直接站在計算機的角度區(qū)抽象問題和解決問題,稱為面向過程的編程思想。
面向過程的編程思想極大提升了現(xiàn)代軟件開放的生產(chǎn)效率和軟件可以達(dá)到的規(guī)模,但現(xiàn)實世界與計算機之間不可能避免存在一些差異。例如,很難想象現(xiàn)實中的對象在一項工作進(jìn)行期間,被不停的中斷和切換,對象的數(shù)據(jù)可能在中斷期間被修改。
對于高效并發(fā)來說,首先需要保證并發(fā)的正確性,在此基礎(chǔ)上實現(xiàn)高效。
2.線程安全
《Java Concurrency In Practice》做的作者"Brian Goetz"對"線程安全"有一個比較恰當(dāng)?shù)亩x:"當(dāng)多個線程訪問一個對象時,如果不考慮這些線程在運行環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個對象的行為都可以獲得正確的結(jié)果,那這個對象是線程安全的"。
2.1Java中的線程安全
按照線程安全的"安全程度"由強至弱來排序,我們可以將Java中各個操作共享的數(shù)據(jù)分為以下5類:不可變、絕對線程安全、相對線程安全、線程兼容、線程對立。
1.不可變
Java中,不可變的對象一定是線程安全的,無論是對象的方法實現(xiàn)還是方法的調(diào)用者,都不用再采取任何線程安全保障措施。
Java中,如果共享數(shù)據(jù)是基本數(shù)據(jù)類型,那么只要再定義時使用final修飾就可以保證它是不可變的。如果共享數(shù)據(jù)是一個對象,就需要保證對象的行為不會對其狀態(tài)產(chǎn)生任何影響才行。保證行為不影響自己狀態(tài)的方法有很多種,最簡單的就是把對象中帶有狀態(tài)的變量都聲明為final。
2.絕對線程安全
絕對線程安全滿足Brian Goetz給出的線程安全定義"不管運行時環(huán)境如何,調(diào)用者都不需要任何額外的同步措施"通常要很大的代價。Java API中標(biāo)注自己是線程安全的類,大多數(shù)不是絕對的線程安全。
下面看一個測試:

運行結(jié)果是:

很明顯,即使它的方法都是同步的,但多線程環(huán)境下,不再方法調(diào)用時做額外的同步措施的話,使用這段代碼仍是不安全的。如果要保證這段代碼正確執(zhí)行,需要修改為以下:

3.相對線程安全
相對線程安全就是通常意義上的線程安全,它需要保證對這個對象單獨的操作是線程安全的,我們在調(diào)用的時候不需要做額外的保障措施,但對于一些特定順序但連續(xù)調(diào)用,可能需要在調(diào)用時使用額外的同步手段保證正確性。
4.線程兼容
指對象本身并不是線程安全的,但是可以通過在調(diào)用時正確的使用同步手段來保證對象在并發(fā)環(huán)境下可以安全的使用,我們平常說一個類不是線程安全的,絕大多數(shù)指的是這種情況。
5.線程對立
指無論在調(diào)用時是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼。
2.2線程安全的實現(xiàn)方法
接下來了解一下代碼編寫如何實現(xiàn)線程安全和虛擬機如何實現(xiàn)同步與鎖機制。
1.互斥同步
常見的并發(fā)正確性保障手段。同步指在多個線程并發(fā)訪問數(shù)據(jù)時,保證共享數(shù)據(jù)在同一個時刻只被一個線程使用。互斥時實現(xiàn)同步的一種手段,臨界區(qū)、互斥量和信號量都是主要的互斥實現(xiàn)方式。因此,互斥是方法,同步是目的。
Java中最基本的互斥同步手段是synchronized,synchronized經(jīng)過編譯后,會在同步塊前后分別形成monitorenter和monitorexit兩個字節(jié)碼指令,這兩個字節(jié)碼都需要一個reference類型的參數(shù)指明要鎖定和解鎖的對象。
根據(jù)規(guī)范的要求,在monitorenter執(zhí)行時,首先嘗試獲取對象的鎖。如果這個對象沒被鎖定,或當(dāng)前線程已經(jīng)擁有了那個對象的鎖,把鎖的計數(shù)器加1;執(zhí)行monitorexit指令將鎖計數(shù)器減1,計數(shù)器為0時,鎖被釋放。如果獲取對象的鎖失敗,當(dāng)前線程被阻塞等待,直到對象鎖被另外一個線程釋放為止。
除了synchronized之外,我們還可以使用java.util.concurrent中的重入鎖(ReentrantLock)實現(xiàn)同步,ReentrantLock和synchronized相似,他們都具備一樣都線程重入特性,只是代碼寫法上有區(qū)別。
不過相比synchronized,ReentrantLock增加了一些高級功能,主要是以下3項:等待可中斷、可實現(xiàn)公平鎖、鎖可以綁定多個條件。
等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情。可中斷特性對處理執(zhí)行時間非常長的同步塊很有幫助。
公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序依次來獲得鎖;非公平鎖:在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖時非公平的,ReentrantLock默認(rèn)也是非公平的,但可以通過帶布爾值的構(gòu)造函數(shù)要求使用公平鎖。
鎖可以綁定多個條件:一個ReentrantLock對象可以同時綁定多個Condition對象,而synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現(xiàn)一個隱含的條件,如果要和多于一個的條件關(guān)聯(lián),就不得不額外添加一個鎖;ReentrantLock只需要多次調(diào)用newCondition()方法即可。
那么基于性能考慮呢?Brian Goetz對這兩種鎖分別在JDK1.5與單核處理器中、JDK1.5與雙Xeon處理器環(huán)境做了一組吞吐量對比的實驗,結(jié)果如下圖所示:

可以看出ReentrantLock在多線程環(huán)境下更穩(wěn)定,與其說ReentrantLock性能好,還不如說synchronized有非常大優(yōu)化大空間,JDK1.6也印證了這一點,人們發(fā)現(xiàn)synchronized和ReentrantLock性能上基本持平了。

2.非阻塞同步
互斥同步最主要的問題是進(jìn)行線程阻塞和喚醒帶來的性能問題,因此這種同步也稱為"阻塞同步",它是一種悲觀的并發(fā)策略,無論共享數(shù)據(jù)是否真的會出現(xiàn)競爭,它都要進(jìn)行加鎖、用戶態(tài)和心態(tài)轉(zhuǎn)換、維護(hù)鎖計數(shù)器和檢查是否有阻塞的線程需要喚醒等。
不過我們還有一種選擇基于沖突檢測的并發(fā)策略:先進(jìn)行操作,如果沒有其他線程爭用共享數(shù)據(jù),那操作就是成功了,如果共享數(shù)據(jù)有爭用,產(chǎn)生沖突就采取其他的補償措施。(最常見的是不斷的重試,知道成功為止),這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要把線程掛起,因此被稱為"非阻塞同步"。
為什么樂觀并發(fā)策略需要"硬件指令集的發(fā)展"才能進(jìn)行?因為我們需要操作和沖突檢測兩個步驟都具備原子性,如果使用互斥同步來保證它們原子性就失去意義了,所以只能靠硬件完成這件事。硬件保證需要多次操作的行為只通過一條處理器指令就能完成,常用指令有:
測試并設(shè)置(Test-and-Set)
獲取并增加(Fetch-and-Increment)
交換(Swap)
比較并交換(Compare-and-Swap,CAS)
加載鏈接/條件存儲(Load-Linked/Store-Conditional,LL/SC)
其中前面3條是早就有的處理器指令,后面2條是現(xiàn)代處理器新增的,這2條指令的目的和功能是類似的。
CAS指令需要3個操作數(shù),分別是內(nèi)存位置(Java中簡單理解為變量的內(nèi)存地址,用V表示)、舊的預(yù)期值(A表示)、新值(B表示)。CAS執(zhí)行時,當(dāng)且僅當(dāng)V符合舊預(yù)期值A(chǔ)時,處理器用新值B更新V的值,否則它不執(zhí)行更新。無論是否更新了V的值,都會返回V的舊值,這個處理過程是一個原子操作。
JDK1.5后,Java才可以使用CAS操作。它們由sun.misc.Unsafe類里面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,由于Unsafe不是提供用戶程序調(diào)用的類,因此如果不采用反射,我們只能通過其他的Java API間接使用它。
我們再來看看12章中沒有解決的代碼問題使用CAS操作避免阻塞同步,我們曾通過20個線程自增的代碼證明volatile變量不具備原子性,那么如何讓他具備呢?把"race++"操作或increase()方法用同步包裝當(dāng)然是一個方法,但如果改成下面代碼效率會提高很多:

使用AtomicInteger代替int后,程序輸出了正確的結(jié)果。

incrementAndGet()方法在一個無限循環(huán)中,不斷嘗試將一個比當(dāng)前值大1的新值賦值給自己。如果失敗了,那說明"獲取-設(shè)置"操作的時候值已經(jīng)有了修改,于是再次循環(huán)進(jìn)行下一次操作,直到設(shè)置成功。
3.無同步方案
要保證線程安全,并不一定要進(jìn)行同步,同步只是保證共享數(shù)據(jù)爭用時的正確性的手段,如果一個方法本來不涉及共享數(shù)據(jù),那它自然無須同步措施。
因此有些代碼天生就是線程安全的:
可重入代碼(Reentrant Code):這種代碼也叫純代碼(Pure Code),可以在代碼執(zhí)行的任何時刻中斷它,轉(zhuǎn)而去執(zhí)行另一段代碼,在控制權(quán)返回后,原來的程序不會出現(xiàn)任何錯誤。
線程本地存儲(Thread Local Storage):如果一段代碼中的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行。如果可以保證,我們就能把共享數(shù)據(jù)的可見范圍限制在同一個線程內(nèi),這樣也無須同步就能保證正確性。
3.鎖優(yōu)化
3.1 自旋鎖與自適應(yīng)自旋
互斥同步對性能最大的影響是阻塞的實現(xiàn),掛起和恢復(fù)線程都需要轉(zhuǎn)入內(nèi)核態(tài)完成。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面請求鎖的線程"等一下",但不放棄處理器的執(zhí)行時間,看看持有鎖的線程是否很快會釋放鎖。為了讓線程等待,我們只需要讓線程執(zhí)行一個忙循環(huán),這被稱為自旋鎖。
自旋鎖本身雖然避免了線程切換的開銷,但它是占用處理器時間的。如果鎖被占用的時間很短,自旋鎖等待的效果就非常好,如果鎖占用時間很長,就會白白浪費處理器資源。因此,自旋鎖等待必須有個限度。默認(rèn)次數(shù)是10此,用戶可以通過使用參數(shù)-XX:PreBlockSpin更改。
3.2鎖消除
鎖消除指虛擬機即時編譯器運行的時候,對一些代碼上要求同步,但是檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行消除。
主要判定依據(jù)是逃逸分析的數(shù)據(jù)支持:如果判斷一段代碼中,堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到,就可以把它們當(dāng)作棧上的數(shù)據(jù)對待,認(rèn)為它們是線程私有的,同步加鎖自然就沒必要了。
許多同步措施并不是程序員自己加入的,同步的代碼在Java程序中很普遍。例如下面的代碼:
public String concatString(String s1, String s2, String s3){
return s1 + s2 + s3;
}
由于String是一個不可變的類,對字符串的操作總是通過生成新的String對象來進(jìn)行,因此Javac編譯器會其做了自動優(yōu)化:
// JDK1.5之前
public String concatString (String s1, String s2, String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每個StringBuffer.append()方法都有一個同步塊,鎖就是sb對象。虛擬機會觀察變量sb,發(fā)現(xiàn)它的動態(tài)作用域被限制在concatString(),其他線程無法訪問到它,因此這里有鎖,可以被安全的消除。
3.3 鎖粗化
原則上,我們推薦將同步塊的作用范圍限制的盡量小——只在共享數(shù)據(jù)的實際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能小,如果存在競爭,等待鎖的線程也可以盡快拿到鎖。
大部分情況,這樣是沒問題的。但如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,甚至加鎖操作出現(xiàn)在循環(huán)體中,即時沒有線程競爭,頻繁互斥同步操作也會導(dǎo)致不必要的性能損耗。
上面的StringBuffer.append()就是這類情況,如果虛擬機檢測到這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部。這樣只需要加鎖一次就可以。
3.4 輕量級鎖
它是JDK1.6中加入的新型鎖機制,"輕量級"是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的,傳統(tǒng)鎖就被稱為"重量級"鎖。
輕量級鎖并不是來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗。
HotSpot虛擬機的對象頭分為兩部分信息,第一部分用于存儲對象自身的運行時數(shù)據(jù),如哈希嗎、GC分代年齡等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機中分別是32bit和64bit,官方叫它"Mark Word"。它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵。第二部分用于存儲指向方法去對象類型數(shù)據(jù)的指針,如果是數(shù)組對象,還會有一個額外的部分用于存儲數(shù)組長度。
對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲盡量多的信息,它會根據(jù)對象狀態(tài)復(fù)用自己的存儲空間。
例如32位的HotSpot虛擬機中對象未鎖定的狀態(tài)下,Mark Word的32bit空間中的25bit用于存儲對象哈希嗎,4bit用于存儲對象分代年齡,2bit用于存儲鎖標(biāo)志位,1bit固定為0,其他狀態(tài)下對象的存儲內(nèi)容如下表所示:

在代碼進(jìn)入到同步塊的時候,如果此同步對象沒有被鎖定(標(biāo)志位"01"),虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)空間,用于存儲鎖對象目前的Mark Word拷貝(稱為"Displaced Mark Word")。這時候線程堆棧與對象頭的狀態(tài)如下圖所示:

然后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針。如果這個更新成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)記位轉(zhuǎn)變?yōu)?00",表示此對象處于輕量級鎖定狀態(tài),如下圖所示:

如果更新操作失敗,虛擬機首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果指了說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明這鎖對象已經(jīng)被其他線程占了。
如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)為"10",Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
它的解鎖過程也是通過CAS操作的,如果對象的Mark Word仍然指向線程的鎖記錄,那就用CAS操作把對象當(dāng)前的Mark Word和線程中復(fù)制的Dispalced Mark Word替換回來。如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
輕量級鎖能提升程序同步性能的依據(jù)是"對于絕大部分的鎖,在整個同步周期內(nèi)都是不存在競爭的"。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷;但如果存在鎖競爭,除了互斥量的開銷,還發(fā)生了額外的CAS操作,因此,競爭情況下,輕量級鎖會更慢。
3.5偏向鎖
它也是JDK1.6引入的,目的是消除數(shù)據(jù)在無競爭情況下的同步,進(jìn)一步提高程序的運行性能。
如果輕量級鎖是在無競爭條件下使用CAS操作消除同步使用的互斥量,那么偏向鎖就是在無競爭情況下把整個同步都消除,CAS操作都不做了。
偏向鎖的偏意思是這個鎖會偏向于第一個獲得它的線程,如果在接下來執(zhí)行中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
假設(shè)虛擬機啟動了偏向鎖,那么當(dāng)鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標(biāo)志位設(shè)為"01"。同時使用CAS操作把獲取到這個鎖的線程ID記錄在對象的Mark Word中,如果CAS操作成功,持有偏向鎖的線程以后每次進(jìn)入這個鎖相關(guān)的同步塊時,虛擬機都可以不再進(jìn)行任何同步操作。
當(dāng)另外一個線程嘗試獲取這個鎖時,偏向模式宣告結(jié)束。根據(jù)鎖對象目前是否處于被鎖定的狀態(tài),撤銷偏向(Revoke Bias)后恢復(fù)到未鎖定(標(biāo)志位"01")或輕量級鎖定(標(biāo)志位"00")。后續(xù)的同步操作就是輕量級那樣執(zhí)行。
偏向鎖、輕量級鎖的狀態(tài)轉(zhuǎn)化及對象Mark Word的關(guān)系如下圖:

偏向鎖可以提高帶有同步但無競爭的程序性能,并不一定總對程序有利。如果程序大多數(shù)鎖總是被多個不同的線程訪問,偏向鎖就是多余的。