0 前言
在單線(xiàn)程中不會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題,而在多線(xiàn)程編程中,有可能會(huì)出現(xiàn)同時(shí)訪(fǎng)問(wèn)同一個(gè) 共享、可變資源 的情況,這種資源可以是:一個(gè)變量、一個(gè)對(duì)象、一個(gè)文件等。特別注意兩點(diǎn):
- 共享: 意味著該資源可以由多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn);
- 可變: 意味著該資源可以在其生命周期內(nèi)被修改;
簡(jiǎn)單的說(shuō),如果你的代碼在單線(xiàn)程下執(zhí)行和在多線(xiàn)程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果,那么你的代碼就是線(xiàn)程安全的。那么,當(dāng)進(jìn)行多線(xiàn)程編程時(shí),我們又會(huì)面臨哪些線(xiàn)程安全的要求呢?又是要如何去解決的呢?
1 線(xiàn)程安全特性
1.1 原子性
跟數(shù)據(jù)庫(kù)事務(wù)的原子性概念差不多,即一個(gè)操作(有可能包含有多個(gè)子操作)要么全部執(zhí)行(生效),要么全部都不執(zhí)行(都不生效)。
關(guān)于原子性,一個(gè)非常經(jīng)典的例子就是銀行轉(zhuǎn)賬問(wèn)題:
比如:A和B同時(shí)向C轉(zhuǎn)賬10萬(wàn)元。如果轉(zhuǎn)賬操作不具有原子性,A在向C轉(zhuǎn)賬時(shí),讀取了C的余額為20萬(wàn),然后加上轉(zhuǎn)賬的10萬(wàn),計(jì)算出此時(shí)應(yīng)該有30萬(wàn),但還未來(lái)及將30萬(wàn)寫(xiě)回C的賬戶(hù),此時(shí)B的轉(zhuǎn)賬請(qǐng)求過(guò)來(lái)了,B發(fā)現(xiàn)C的余額為20萬(wàn),然后將其加10萬(wàn)并寫(xiě)回。然后A的轉(zhuǎn)賬操作繼續(xù)——將30萬(wàn)寫(xiě)回C的余額。這種情況下C的最終余額為30萬(wàn),而非預(yù)期的40萬(wàn)。
1.2 可見(jiàn)性
可見(jiàn)性是指,當(dāng)多個(gè)線(xiàn)程并發(fā)訪(fǎng)問(wèn)共享變量時(shí),一個(gè)線(xiàn)程對(duì)共享變量的修改,其它線(xiàn)程能夠立即看到??梢?jiàn)性問(wèn)題是好多人忽略或者理解錯(cuò)誤的一點(diǎn)。
CPU從主內(nèi)存中讀數(shù)據(jù)的效率相對(duì)來(lái)說(shuō)不高,現(xiàn)在主流的計(jì)算機(jī)中,都有幾級(jí)緩存。每個(gè)線(xiàn)程讀取共享變量時(shí),都會(huì)將該變量加載進(jìn)其對(duì)應(yīng)CPU的高速緩存里,修改該變量后,CPU會(huì)立即更新該緩存,但并不一定會(huì)立即將其寫(xiě)回主內(nèi)存(實(shí)際上寫(xiě)回主內(nèi)存的時(shí)間不可預(yù)期)。此時(shí)其它線(xiàn)程(尤其是不在同一個(gè)CPU上執(zhí)行的線(xiàn)程)訪(fǎng)問(wèn)該變量時(shí),從主內(nèi)存中讀到的就是舊的數(shù)據(jù),而非第一個(gè)線(xiàn)程更新后的數(shù)據(jù)。
這一點(diǎn)是操作系統(tǒng)或者說(shuō)是硬件層面的機(jī)制,所以很多應(yīng)用開(kāi)發(fā)人員經(jīng)常會(huì)忽略。
1.3 有序性
有序性指的是,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。以下面這段代碼為例:
boolean started = false; // 語(yǔ)句1
long counter = 0L; // 語(yǔ)句2
counter = 1; // 語(yǔ)句3
started = true; // 語(yǔ)句4
從代碼順序上看,上面四條語(yǔ)句應(yīng)該依次執(zhí)行,但實(shí)際上JVM真正在執(zhí)行這段代碼時(shí),并不保證它們一定完全按照此順序執(zhí)行。
處理器為了提高程序整體的執(zhí)行效率,可能會(huì)對(duì)代碼進(jìn)行優(yōu)化,其中的一項(xiàng)優(yōu)化方式就是調(diào)整代碼順序,按照更高效的順序執(zhí)行代碼。
講到這里,有人要著急了——什么,CPU不按照我的代碼順序執(zhí)行代碼,那怎么保證得到我們想要的效果呢?實(shí)際上,大家大可放心,CPU雖然并不保證完全按照代碼順序執(zhí)行,但它會(huì)保證程序最終的執(zhí)行結(jié)果和代碼順序執(zhí)行時(shí)的結(jié)果一致。
2 線(xiàn)程安全問(wèn)題
2.1 競(jìng)態(tài)條件與臨界區(qū)
線(xiàn)程之間共享堆空間,在編程的時(shí)候就要格外注意避免競(jìng)態(tài)條件。危險(xiǎn)在于多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)相同的資源并進(jìn)行讀寫(xiě)操作。當(dāng)其中一個(gè)線(xiàn)程需要根據(jù)某個(gè)變量的狀態(tài)來(lái)相應(yīng)執(zhí)行某個(gè)操作的之前,該變量很可能已經(jīng)被其它線(xiàn)程修改。
也就是說(shuō),當(dāng)兩個(gè)線(xiàn)程競(jìng)爭(zhēng)同一資源時(shí),如果對(duì)資源的訪(fǎng)問(wèn)順序敏感,就稱(chēng)存在 競(jìng)態(tài)條件。導(dǎo)致竟態(tài)條件發(fā)生的代碼稱(chēng)作 臨界區(qū)。
/**
* 以下這段代碼就存在競(jìng)態(tài)條件,其中return ++count就是臨界區(qū)。
*/
public class Obj
{
private int count;
public int incr()
{
return ++count;
}
}
2.2 死鎖
死鎖:指兩個(gè)或兩個(gè)以上的進(jìn)程(或線(xiàn)程)在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的一種互相等待的現(xiàn)象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去。此時(shí)稱(chēng)系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程稱(chēng)為死鎖進(jìn)程。
關(guān)于死鎖發(fā)生的條件:
- 互斥條件:線(xiàn)程對(duì)資源的訪(fǎng)問(wèn)是排他性的,如果一個(gè)線(xiàn)程對(duì)占用了某資源,那么其他線(xiàn)程必須處于等待狀態(tài),直到資源被釋放。
- 請(qǐng)求和保持條件:線(xiàn)程T1至少已經(jīng)保持了一個(gè)資源R1占用,但又提出對(duì)另一個(gè)資源R2請(qǐng)求,而此時(shí),資源R2被其他線(xiàn)程T2占用,于是該線(xiàn)程T1也必須等待,但又對(duì)自己保持的資源R1不釋放。
- 不剝奪條件:線(xiàn)程已獲得的資源,在未使用完之前,不能被其他線(xiàn)程剝奪,只能在使用完以后由自己釋放。
- 環(huán)路等待條件:在死鎖發(fā)生時(shí),必然存在一個(gè)“進(jìn)程-資源環(huán)形鏈”,即:
{p0,p1,p2,...pn},進(jìn)程p0(或線(xiàn)程)等待p1占用的資源,p1等待p2占用的資源,pn等待p0占用的資源。(最直觀的理解是,p0等待p1占用的資源,而p1而在等待p0占用的資源,于是兩個(gè)進(jìn)程就相互等待)。
2.3 活鎖
活鎖:是指線(xiàn)程1可以使用資源,但它很禮貌,讓其他線(xiàn)程先使用資源,線(xiàn)程2也可以使用資源,但它很紳士,也讓其他線(xiàn)程先使用資源。這樣你讓我,我讓你,最后兩個(gè)線(xiàn)程都無(wú)法使用資源。
關(guān)于“死鎖與活鎖”的比喻:
死鎖:迎面開(kāi)來(lái)的汽車(chē)A和汽車(chē)B過(guò)馬路,汽車(chē)A得到了半條路的資源(滿(mǎn)足死鎖發(fā)生條件1:資源訪(fǎng)問(wèn)是排他性的,我占了路你就不能上來(lái),除非你爬我頭上去),汽車(chē)B占了汽車(chē)A的另外半條路的資源,A想過(guò)去必須請(qǐng)求另一半被B占用的道路(死鎖發(fā)生條件2:必須整條車(chē)身的空間才能開(kāi)過(guò)去,我已經(jīng)占了一半,尼瑪另一半的路被B占用了),B若想過(guò)去也必須等待A讓路,A是輛蘭博基尼,B是開(kāi)奇瑞QQ的屌絲,A素質(zhì)比較低開(kāi)窗對(duì)B狂罵:快給老子讓開(kāi),B很生氣,你媽逼的,老子就不讓?zhuān)ㄋ梨i發(fā)生條件3:在未使用完資源前,不能被其他線(xiàn)程剝奪),于是兩者相互僵持一個(gè)都走不了(死鎖發(fā)生條件4:環(huán)路等待條件),而且導(dǎo)致整條道上的后續(xù)車(chē)輛也走不了。
活鎖:馬路中間有條小橋,只能容納一輛車(chē)經(jīng)過(guò),橋兩頭開(kāi)來(lái)兩輛車(chē)A和B,A比較禮貌,示意B先過(guò),B也比較禮貌,示意A先過(guò),結(jié)果兩人一直謙讓誰(shuí)也過(guò)不去。
2.4 饑餓
饑餓:是指如果線(xiàn)程T1占用了資源R,線(xiàn)程T2又請(qǐng)求封鎖R,于是T2等待。T3也請(qǐng)求資源R,當(dāng)T1釋放了R上的封鎖后,系統(tǒng)首先批準(zhǔn)了T3的請(qǐng)求,T2仍然等待。然后T4又請(qǐng)求封鎖R,當(dāng)T3釋放了R上的封鎖之后,系統(tǒng)又批準(zhǔn)了T4的請(qǐng)求......,T2可能永遠(yuǎn)等待。
也就是,如果一個(gè)線(xiàn)程因?yàn)镃PU時(shí)間全部被其他線(xiàn)程搶走而得不到CPU運(yùn)行時(shí)間,這種狀態(tài)被稱(chēng)之為“饑餓”。而該線(xiàn)程被“饑餓致死”正是因?yàn)樗貌坏紺PU運(yùn)行時(shí)間的機(jī)會(huì)。
關(guān)于“饑餓”的比喻:
在“首堵”北京的某一天,天氣陰沉,空氣中充斥著霧霾和地溝油的味道,某個(gè)苦逼的臨時(shí)工交警正在處理塞車(chē),有兩條道A和B上都堵滿(mǎn)了車(chē)輛,其中A道堵的時(shí)間最長(zhǎng),B相對(duì)堵的時(shí)間較短,這時(shí),前面道路已疏通,交警按照最佳分配原則,示意B道上車(chē)輛先過(guò),B道路上過(guò)了一輛又一輛,A道上排隊(duì)時(shí)間最長(zhǎng)的卻沒(méi)法通過(guò),只能等B道上沒(méi)有車(chē)輛通過(guò)的時(shí)候再等交警發(fā)指令讓A道依次通過(guò),這也就是ReentrantLock顯示鎖里提供的不公平鎖機(jī)制(當(dāng)然了,ReentrantLock也提供了公平鎖的機(jī)制,由用戶(hù)根據(jù)具體的使用場(chǎng)景而決定到底使用哪種鎖策略),不公平鎖能夠提高吞吐量但不可避免的會(huì)造成某些線(xiàn)程的饑餓。
在Java中,下面三個(gè)常見(jiàn)的原因會(huì)導(dǎo)致線(xiàn)程饑餓,如下:
-
高優(yōu)先級(jí)線(xiàn)程吞噬所有的低優(yōu)先級(jí)線(xiàn)程的CPU時(shí)間
你能為每個(gè)線(xiàn)程設(shè)置獨(dú)自的線(xiàn)程優(yōu)先級(jí),優(yōu)先級(jí)越高的線(xiàn)程獲得的CPU時(shí)間越多,線(xiàn)程優(yōu)先級(jí)值設(shè)置在1到10之間,而這些優(yōu)先級(jí)值所表示行為的準(zhǔn)確解釋則依賴(lài)于你的應(yīng)用運(yùn)行平臺(tái)。對(duì)大多數(shù)應(yīng)用來(lái)說(shuō),你最好是不要改變其優(yōu)先級(jí)值。
-
線(xiàn)程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài),因?yàn)槠渌€(xiàn)程總是能在它之前持續(xù)地對(duì)該同步塊進(jìn)行訪(fǎng)問(wèn)
Java的同步代碼區(qū)也是一個(gè)導(dǎo)致饑餓的因素。Java的同步代碼區(qū)對(duì)哪個(gè)線(xiàn)程允許進(jìn)入的次序沒(méi)有任何保障。這就意味著理論上存在一個(gè)試圖進(jìn)入該同步區(qū)的線(xiàn)程處于被永久堵塞的風(fēng)險(xiǎn),因?yàn)槠渌€(xiàn)程總是能持續(xù)地先于它獲得訪(fǎng)問(wèn),這即是“饑餓”問(wèn)題,而一個(gè)線(xiàn)程被“饑餓致死”正是因?yàn)樗貌坏紺PU運(yùn)行時(shí)間的機(jī)會(huì)。
-
線(xiàn)程在等待一個(gè)本身(在其上調(diào)用wait())也處于永久等待完成的對(duì)象,因?yàn)槠渌€(xiàn)程總是被持續(xù)地獲得喚醒
如果多個(gè)線(xiàn)程處在wait()方法執(zhí)行上,而對(duì)其調(diào)用notify()不會(huì)保證哪一個(gè)線(xiàn)程會(huì)獲得喚醒,任何線(xiàn)程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個(gè)風(fēng)險(xiǎn):一個(gè)等待線(xiàn)程從來(lái)得不到喚醒,因?yàn)槠渌却€(xiàn)程總是能被獲得喚醒。
2.5 公平
解決饑餓的方案被稱(chēng)之為“公平性” – 即所有線(xiàn)程均能公平地獲得運(yùn)行機(jī)會(huì)。在Java中實(shí)現(xiàn)公平性方案,需要:
- 使用鎖,而不是同步塊;
- 使用公平鎖;
- 注意性能方面;
在Java中實(shí)現(xiàn)公平性,雖Java不可能實(shí)現(xiàn)100%的公平性,依然可以通過(guò)同步結(jié)構(gòu)在線(xiàn)程間實(shí)現(xiàn)公平性的提高。
首先來(lái)學(xué)習(xí)一段簡(jiǎn)單的同步態(tài)代碼:
public class Synchronizer{
public synchronized void doSynchronized () {
// do a lot of work which takes a long time
}
}
如果有多個(gè)線(xiàn)程調(diào)用doSynchronized()方法,在第一個(gè)獲得訪(fǎng)問(wèn)的線(xiàn)程未完成前,其他線(xiàn)程將一直處于阻塞狀態(tài),而且在這種多線(xiàn)程被阻塞的場(chǎng)景下,接下來(lái)將是哪個(gè)線(xiàn)程獲得訪(fǎng)問(wèn)是沒(méi)有保障的。
改為 使用鎖方式替代同步塊,為了提高等待線(xiàn)程的公平性,我們使用鎖方式來(lái)替代同步塊:
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
//critical section, do a lot of work which takes a long time
this.lock.unlock();
}
}
注意到doSynchronized()不再聲明為synchronized,而是用lock.lock()和lock.unlock()來(lái)替代。下面是用Lock類(lèi)做的一個(gè)實(shí)現(xiàn):
public class Lock{
private boolean isLocked = false;
private Thread lockingThread = null;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
注意到上面對(duì)Lock的實(shí)現(xiàn),如果存在多線(xiàn)程并發(fā)訪(fǎng)問(wèn)lock(),這些線(xiàn)程將阻塞在對(duì)lock()方法的訪(fǎng)問(wèn)上。另外,如果鎖已經(jīng)鎖上(校對(duì)注:這里指的是isLocked等于true時(shí)),這些線(xiàn)程將阻塞在while(isLocked)循環(huán)的wait()調(diào)用里面。要記住的是,當(dāng)線(xiàn)程正在等待進(jìn)入lock() 時(shí),可以調(diào)用wait()釋放其鎖實(shí)例對(duì)應(yīng)的同步鎖,使得其他多個(gè)線(xiàn)程可以進(jìn)入lock()方法,并調(diào)用wait()方法。
這回看下doSynchronized(),你會(huì)注意到在lock()和unlock()之間的注釋?zhuān)涸谶@兩個(gè)調(diào)用之間的代碼將運(yùn)行很長(zhǎng)一段時(shí)間。進(jìn)一步設(shè)想,這段代碼將長(zhǎng)時(shí)間運(yùn)行,和進(jìn)入lock()并調(diào)用wait()來(lái)比較的話(huà)。這意味著大部分時(shí)間用在等待進(jìn)入鎖和進(jìn)入臨界區(qū)的過(guò)程是用在wait()的等待中,而不是被阻塞在試圖進(jìn)入lock()方法中。
在早些時(shí)候提到過(guò),同步塊不會(huì)對(duì)等待進(jìn)入的多個(gè)線(xiàn)程誰(shuí)能獲得訪(fǎng)問(wèn)做任何保障,同樣當(dāng)調(diào)用notify()時(shí),wait()也不會(huì)做保障一定能喚醒線(xiàn)程。因此這個(gè)版本的Lock類(lèi)和doSynchronized()那個(gè)版本就保障公平性而言,沒(méi)有任何區(qū)別。
但我們能夠改變這種情況,如下:
當(dāng)前的Lock類(lèi)版本調(diào)用自己的wait()方法,如果每個(gè)線(xiàn)程在不同的對(duì)象上調(diào)用wait(),那么只有一個(gè)線(xiàn)程會(huì)在該對(duì)象上調(diào)用wait(),Lock類(lèi)可以決定哪個(gè)對(duì)象能對(duì)其調(diào)用notify(),因此能做到有效的選擇喚醒哪個(gè)線(xiàn)程。
下面將上面Lock類(lèi)轉(zhuǎn)變?yōu)楣芥iFairLock。你會(huì)注意到新的實(shí)現(xiàn)和之前的Lock類(lèi)中的同步和wait()/notify()稍有不同。重點(diǎn)是,每一個(gè)調(diào)用lock()的線(xiàn)程都會(huì)進(jìn)入一個(gè)隊(duì)列,當(dāng)解鎖時(shí),只有隊(duì)列里的第一個(gè)線(xiàn)程被允許鎖住FairLock實(shí)例,所有其它的線(xiàn)程都將處于等待狀態(tài),直到他們處于隊(duì)列頭部。如下:
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();
public void lock() throws InterruptedException{
// 當(dāng)前線(xiàn)程創(chuàng)建“令牌”
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized(this){
// 所有線(xiàn)程的queueObject令牌,入隊(duì)
waitingThreads.add(queueObject);
}
while(isLockedForThisThread){
synchronized(this){
// 1. 判斷是否已被鎖住:是否已有線(xiàn)程獲得鎖,正在執(zhí)行同步代碼塊
// 2. 判斷頭部令牌與當(dāng)前線(xiàn)程令牌是否一致:也就是只鎖住頭部令牌對(duì)應(yīng)的線(xiàn)程;
isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
if(!isLockedForThisThread){
isLocked = true;
// 移除頭部令牌
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try{
// 其他線(xiàn)程執(zhí)行doWait(),進(jìn)行等待
queueObject.doWait();
}catch(InterruptedException e){
synchronized(this) { waitingThreads.remove(queueObject); }
throw e;
}
}
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if(waitingThreads.size() > 0) {
// 喚醒頭部令牌對(duì)應(yīng)的線(xiàn)程,可以執(zhí)行
waitingThreads.get(0).doNotify();
}
}
}
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while(!isNotified){
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
首先注意到lock()方法不在聲明為synchronized,取而代之的是對(duì)必需同步的代碼,在synchronized中進(jìn)行嵌套。
FairLock新創(chuàng)建了一個(gè)QueueObject的實(shí)例,并對(duì)每個(gè)調(diào)用lock()的線(xiàn)程進(jìn)行入隊(duì)操作。調(diào)用unlock()的線(xiàn)程將從隊(duì)列頭部獲取QueueObject,并對(duì)其調(diào)用doNotify(),以喚醒在該對(duì)象上等待的線(xiàn)程。通過(guò)這種方式,在同一時(shí)間僅有一個(gè)等待線(xiàn)程獲得喚醒,而不是所有的等待線(xiàn)程。這也是實(shí)現(xiàn)FairLock公平性的核心所在。
還需注意到,QueueObject實(shí)際是一個(gè)semaphore。doWait()和doNotify()方法在QueueObject中保存著信號(hào)。這樣做以避免一個(gè)線(xiàn)程在調(diào)用queueObject.doWait()之前被另一個(gè)線(xiàn)程調(diào)用unlock()并隨之調(diào)用queueObject.doNotify()的線(xiàn)程重入,從而導(dǎo)致信號(hào)丟失。queueObject.doWait()調(diào)用放置在synchronized(this)塊之外,以避免被monitor嵌套鎖死,所以另外的線(xiàn)程可以解鎖,只要當(dāng)沒(méi)有線(xiàn)程在lock方法的synchronized(this)塊中執(zhí)行即可。
最后,注意到queueObject.doWait()在try – catch塊中是怎樣調(diào)用的。在InterruptedException拋出的情況下,線(xiàn)程得以離開(kāi)lock(),并需讓它從隊(duì)列中移除。
3 如何確保線(xiàn)程安全特性
3.1 如何確保原子性
3.1.1 鎖和同步
常用的保證Java操作原子性的工具是 鎖和同步方法(或者同步代碼塊)。使用鎖,可以保證同一時(shí)間只有一個(gè)線(xiàn)程能拿到鎖,也就保證了同一時(shí)間只有一個(gè)線(xiàn)程能執(zhí)行申請(qǐng)鎖和釋放鎖之間的代碼。
public void testLock () {
lock.lock();
try{
int j = i;
i = j + 1;
} finally {
lock.unlock();
}
}
與鎖類(lèi)似的是同步方法或者同步代碼塊。使用非靜態(tài)同步方法時(shí),鎖住的是當(dāng)前實(shí)例;使用靜態(tài)同步方法時(shí),鎖住的是該類(lèi)的Class對(duì)象;使用靜態(tài)代碼塊時(shí),鎖住的是synchronized關(guān)鍵字后面括號(hào)內(nèi)的對(duì)象。下面是同步代碼塊示例:
public void testLock () {
synchronized (anyObject){
int j = i;
i = j + 1;
}
}
無(wú)論使用鎖還是synchronized,本質(zhì)都是一樣,通過(guò)鎖或同步來(lái)實(shí)現(xiàn)資源的排它性,從而實(shí)際目標(biāo)代碼段同一時(shí)間只會(huì)被一個(gè)線(xiàn)程執(zhí)行,進(jìn)而保證了目標(biāo)代碼段的原子性。這是一種以犧牲性能為代價(jià)的方法。
3.1.2 CAS(compare and swap)
基礎(chǔ)類(lèi)型變量自增(i++)是一種常被新手誤以為是原子操作而實(shí)際不是的操作。Java中提供了對(duì)應(yīng)的原子操作類(lèi)來(lái)實(shí)現(xiàn)該操作,并保證原子性,其本質(zhì)是利用了CPU級(jí)別的CAS指令。由于是CPU級(jí)別的指令,其開(kāi)銷(xiāo)比需要操作系統(tǒng)參與的鎖的開(kāi)銷(xiāo)小。AtomicInteger使用方法如下:
AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
new Thread(() -> {
for(int a = 0; a < iteration; a++) {
atomicInteger.incrementAndGet();
}
}).start();
}
3.2 如何確??梢?jiàn)性
Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。當(dāng)使用volatile修飾某個(gè)變量時(shí),它會(huì)保證對(duì)該變量的修改會(huì)立即被更新到內(nèi)存中,并且將其它線(xiàn)程緩存中對(duì)該變量的緩存設(shè)置成無(wú)效,因此其它線(xiàn)程需要讀取該值時(shí)必須從主內(nèi)存中讀取,從而得到最新的值。
volatile適用場(chǎng)景:volatile適用于不需要保證原子性,但卻需要保證可見(jiàn)性的場(chǎng)景。一種典型的使用場(chǎng)景是用它修飾用于停止線(xiàn)程的狀態(tài)標(biāo)記。如下所示:
boolean isRunning = false;
public void start () {
new Thread( () -> {
while(isRunning) {
someOperation();
}
}).start();
}
public void stop () {
isRunning = false;
}
在這種實(shí)現(xiàn)方式下,即使其它線(xiàn)程通過(guò)調(diào)用stop()方法將isRunning設(shè)置為false,循環(huán)也不一定會(huì)立即結(jié)束。可以通過(guò)volatile關(guān)鍵字,保證while循環(huán)及時(shí)得到isRunning最新的狀態(tài)從而及時(shí)停止循環(huán),結(jié)束線(xiàn)程。
3.3 如何確保有序性
上文講過(guò)編譯器和處理器對(duì)指令進(jìn)行重新排序時(shí),會(huì)保證重新排序后的執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致,所以重新排序過(guò)程并不會(huì)影響單線(xiàn)程程序的執(zhí)行,卻可能影響多線(xiàn)程程序并發(fā)執(zhí)行的正確性。
Java中可通過(guò)volatile在一定程序上保證順序性,另外還可以通過(guò)synchronized和鎖來(lái)保證順序性。
synchronized和鎖保證順序性的原理和保證原子性一樣,都是通過(guò)保證同一時(shí)間只會(huì)有一個(gè)線(xiàn)程執(zhí)行目標(biāo)代碼段來(lái)實(shí)現(xiàn)的。
除了從應(yīng)用層面保證目標(biāo)代碼段執(zhí)行的順序性外,JVM還通過(guò)被稱(chēng)為happens-before原則隱式地保證順序性。兩個(gè)操作的執(zhí)行順序只要可以通過(guò)happens-before推導(dǎo)出來(lái),則JVM會(huì)保證其順序性,反之JVM對(duì)其順序性不作任何保證,可對(duì)其進(jìn)行任意必要的重新排序以獲取高效率。
happens-before原則(先行發(fā)生原則),如下:
- 傳遞規(guī)則:如果操作1在操作2前面,而操作2在操作3前面,則操作1肯定會(huì)在操作3前發(fā)生。該規(guī)則說(shuō)明了happens-before原則具有傳遞性。
- 鎖定規(guī)則:一個(gè)unlock操作肯定會(huì)在后面對(duì)同一個(gè)鎖的lock操作前發(fā)生。這個(gè)很好理解,鎖只有被釋放了才會(huì)被再次獲取。
- volatile變量規(guī)則:對(duì)一個(gè)被volatile修飾的寫(xiě)操作先發(fā)生于后面對(duì)該變量的讀操作。
- 程序次序規(guī)則:一個(gè)線(xiàn)程內(nèi),按照代碼順序執(zhí)行。
- 線(xiàn)程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先發(fā)生于此線(xiàn)程的其它動(dòng)作。
- 線(xiàn)程終結(jié)原則:線(xiàn)程的終止檢測(cè)后發(fā)生于線(xiàn)程中其它的所有操作。
- 線(xiàn)程中斷規(guī)則: 對(duì)線(xiàn)程interrupt()方法的調(diào)用先發(fā)生于對(duì)該中斷異常的獲取。
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象構(gòu)造先于它的finalize發(fā)生。
4 關(guān)于線(xiàn)程安全的幾個(gè)為什么
-
平時(shí)項(xiàng)目中使用鎖和synchronized比較多,而很少使用volatile,難道就沒(méi)有保證可見(jiàn)性?
鎖和synchronized即可以保證原子性,也可以保證可見(jiàn)性。都是通過(guò)保證同一時(shí)間只有一個(gè)線(xiàn)程執(zhí)行目標(biāo)代碼段來(lái)實(shí)現(xiàn)的。
-
鎖和synchronized為何能保證可見(jiàn)性?
根據(jù)JDK 7的Java doc中對(duì)
concurrent包的說(shuō)明,一個(gè)線(xiàn)程的寫(xiě)結(jié)果保證對(duì)另外線(xiàn)程的讀操作可見(jiàn),只要該寫(xiě)操作可以由happen-before原則推斷出在讀操作之前發(fā)生。 -
既然鎖和synchronized即可保證原子性也可保證可見(jiàn)性,為何還需要volatile?
synchronized和鎖需要通過(guò)操作系統(tǒng)來(lái)仲裁誰(shuí)獲得鎖,開(kāi)銷(xiāo)比較高,而volatile開(kāi)銷(xiāo)小很多。因此在只需要保證可見(jiàn)性的條件下,使用volatile的性能要比使用鎖和synchronized高得多。
-
既然鎖和synchronized可以保證原子性,為什么還需要AtomicInteger這種的類(lèi)來(lái)保證原子操作?
鎖和synchronized需要通過(guò)操作系統(tǒng)來(lái)仲裁誰(shuí)獲得鎖,開(kāi)銷(xiāo)比較高,而AtomicInteger是通過(guò)CPU級(jí)的CAS操作來(lái)保證原子性,開(kāi)銷(xiāo)比較小。所以使用AtomicInteger的目的還是為了提高性能。
-
還有沒(méi)有別的辦法保證線(xiàn)程安全?
有。盡可能避免引起非線(xiàn)程安全的條件——共享變量。如果能從設(shè)計(jì)上避免共享變量的使用,即可避免非線(xiàn)程安全的發(fā)生,也就無(wú)須通過(guò)鎖或者synchronized以及volatile解決原子性、可見(jiàn)性和順序性的問(wèn)題。
-
synchronized即可修飾非靜態(tài)方式,也可修飾靜態(tài)方法,還可修飾代碼塊,有何區(qū)別?
synchronized修飾非靜態(tài)同步方法時(shí),鎖住的是當(dāng)前實(shí)例;synchronized修飾靜態(tài)同步方法時(shí),鎖住的是該類(lèi)的Class對(duì)象;synchronized修飾靜態(tài)代碼塊時(shí),鎖住的是synchronized關(guān)鍵字后面括號(hào)內(nèi)的對(duì)象。