Java中的鎖

這是一篇關(guān)于java鎖的總結(jié)的開端,后續(xù)會單獨(dú)對部分鎖的原理進(jìn)行展開。內(nèi)容大多來自《深入理解Java虛擬機(jī)》、《Java并發(fā)編程實(shí)戰(zhàn)》和網(wǎng)絡(luò)上。

公平鎖和非公平鎖

顧名思義,一個(gè)是不可搶占嚴(yán)格按照先到先得的鎖,一個(gè)是可搶占的不公平的鎖。
公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請鎖的先后順序來一次獲得鎖。公平鎖的好處是等待鎖的線程不會餓死,但是整體效率相對低一些;
非公平鎖的好處是整體效率相對高一些,但是有些線程可能會餓死或者要等很久才會獲得鎖。如果在某個(gè)時(shí)刻有線程需要獲取鎖,而這個(gè)時(shí)候剛好鎖可用,那么這個(gè)線程會直接搶占,而這時(shí)阻塞在等待隊(duì)列的線程則不會被喚醒。
公平鎖可以使用new ReentrantLock(true)實(shí)現(xiàn)。

可重入鎖

可重入鎖,也叫做遞歸鎖,指的是同一線程外層函數(shù)獲得鎖之后 ,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響。
在JAVA環(huán)境下 ReentrantLock 和synchronized 都是可重入鎖??芍厝腈i最大的作用是避免死鎖。

重入的一種實(shí)現(xiàn)方法是:為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)器和一個(gè)所有者線程。當(dāng)計(jì)數(shù)器值為0時(shí),這個(gè)鎖就被認(rèn)為是沒有被任何線程持有。當(dāng)線程請求一個(gè)未被持有的鎖時(shí),JVM記下鎖的持有者,并將計(jì)數(shù)器置1,如果同一個(gè)線程再次獲取到鎖,計(jì)數(shù)器遞增,當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)器會相應(yīng)地遞減。當(dāng)計(jì)數(shù)器值為0時(shí),這個(gè)鎖將被釋放。
不然當(dāng)子類改寫了父類的synchronized方法,然后調(diào)用父類中的方法,此時(shí)如果沒有可重入鎖,那么這段代碼將會產(chǎn)生死鎖,如:

public class Widget {
    public synchronized void doSomething() {
    ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
    super.doSomething();
    }
}

鎖消除

鎖消除是虛擬機(jī)在運(yùn)行時(shí),對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行消除。鎖消除的主要判斷依據(jù)是來源于逃逸分析的數(shù)據(jù)支持,如果判斷在一段代碼中,堆上的所有數(shù)據(jù)都不會逃逸出去從而能被其他線程訪問到,那就可以把他們當(dāng)做棧上數(shù)據(jù)對待,認(rèn)為他們是線程私有的,同步加鎖自然就無需進(jìn)行??蠢樱?/p>

public String concatString(String s1, String s2, String s3) { 
    StringBuffer sb StringBuffer(); 
    sb.append(s1); 
    sb.append(s2); 
    sb.append(s3); 
    return sb.toString(); 
}

StringBuffer append方法的內(nèi)部加了同步關(guān)鍵字:

public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

也就是說在concatString()方法中涉及了同步操作。但是虛擬機(jī)觀察sb變量的的作用域被限制在方法的內(nèi)部,也就是sb的所有引用不會“逃逸”到concatString之外,其他線程無法訪問到它。因此,雖然這里有鎖,但是可以被安全的消除,在即時(shí)編譯之后,這段代碼就會忽略掉所有的同步而直接執(zhí)行了。

鎖粗化

原則上,我們在編寫代碼的時(shí)候,總是推薦將同步塊的作用范圍限制的盡量小——只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競爭,那等待的線程也能盡快拿到鎖。大部分情況下,這些都是正確的。但是,如果一些列的聯(lián)系操作都是同一個(gè)對象反復(fù)加上和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那么即使沒有線程競爭,頻繁地進(jìn)行互斥同步操作也導(dǎo)致不必要的性能損耗。
舉個(gè)案例,類似鎖消除的concatString()方法。如果StringBuffer sb = new StringBuffer();定義在方法體之外,那么就會有線程競爭,但是每個(gè)append()操作都對同一個(gè)對象反復(fù)加鎖解鎖,那么虛擬機(jī)探測到有這樣的情況的話,會把加鎖同步的范圍擴(kuò)展到整個(gè)操作序列的外部,即擴(kuò)展到第一個(gè)append()操作之前和最后一個(gè)append()操作之后,這樣的一個(gè)鎖范圍擴(kuò)展的操作就稱之為鎖粗化。

類鎖和對象鎖

類鎖:在方法上加上static synchronized的鎖,或者synchronized(xxx.class)的鎖,鎖的范圍是整個(gè)類class。如下代碼中的m1和m2:

對象鎖:鎖對象是當(dāng)前類對象或者自定義鎖對象,參考m3,m4,m5。

public class LockClass { 
    public Object lock = Object(); 
    //類鎖
    public static synchronized m1(){}  
    public m2(){ synchronized(LockClass.class){}} 
    //對象鎖
    public synchronized m3(){}
    public m4() { synchronized(){} } 
    public m5() { synchronized(object1){} } 
}

自旋鎖

Java的線程是映射到操作系統(tǒng)的原生線程之上的,如果要阻塞或喚醒一個(gè)線程,都需要操作系統(tǒng)來幫忙完成,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)中,因此狀態(tài)裝換需要耗費(fèi)很多的處理器時(shí)間,對于代碼簡單的同步塊(如被synchronized修飾的getter()和setter()方法),狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長。

在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時(shí)間,為了這段時(shí)間取掛起和恢復(fù)現(xiàn)場并不值得。
如果物理機(jī)器有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請求鎖的那個(gè)線程“稍等一下“,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。

自旋等待不能代替阻塞。自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時(shí)間的,因此,如果鎖被占用的時(shí)間很短,自旋當(dāng)代的效果就會非常好,反之,如果鎖被占用的時(shí)間很長,那么自旋的線程只會白白浪費(fèi)處理器資源。因此,自旋等待的時(shí)間必須要有一定的限度,如果自旋超過了限定次數(shù)(默認(rèn)是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了。

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK6中已經(jīng)變?yōu)槟J(rèn)開啟,并且引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不在固定了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定。

自旋是在輕量級鎖中使用的,在重量級鎖中,線程不使用自旋。

如果在同一個(gè)鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對更長的時(shí)間,比如100次循環(huán)。另外,如果對于某個(gè)鎖,自旋很少成功獲得過,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,以避免浪費(fèi)處理器資源。

偏向鎖、輕量級鎖和重量級鎖

偏向鎖是JDK6中引入的一項(xiàng)鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無競爭情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能。
大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。
偏向鎖會偏向于第一個(gè)獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要同步。

當(dāng)鎖對象第一次被線程獲取的時(shí)候,線程使用CAS操作把這個(gè)鎖的線程ID記錄再對象Mark Word之中,同時(shí)置偏向標(biāo)志位1。以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。

如果線程使用CAS操作時(shí)失敗則表示該鎖對象上存在競爭并且這個(gè)時(shí)候另外一個(gè)線程獲得偏向鎖的所有權(quán)。當(dāng)?shù)竭_(dá)全局安全點(diǎn)時(shí)獲得偏向鎖的線程被掛起,膨脹為輕量級鎖,同時(shí)被撤銷偏向鎖的線程繼續(xù)往下執(zhí)行同步代碼。
當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束。

線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄(Lock Record)的空間,并將對象頭中的Mard Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。如果自旋失敗則鎖會膨脹成重量級鎖。如果自旋成功則依然處于輕量級鎖的狀態(tài)。

輕量級鎖提升程序同步性能的依據(jù)是:對于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競爭的(區(qū)別于偏向鎖)。這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發(fā)生了CAS操作,因此在有競爭的情況下,輕量級鎖比傳統(tǒng)的重量級鎖更慢。

整個(gè)synchronized鎖流程如下:
1. 檢測Mark Word里面是不是當(dāng)前線程的ID,如果是,表示當(dāng)前線程處于偏向鎖
2. 如果不是,則使用CAS將當(dāng)前線程的ID替換Mard Word,如果成功則表示當(dāng)前線程獲得偏向鎖,置偏向標(biāo)志位1
3. 如果失敗,則說明發(fā)生競爭,撤銷偏向鎖,進(jìn)而升級為輕量級鎖。
4. 當(dāng)前線程使用CAS將對象頭的Mark Word替換為鎖記錄指針,如果成功,當(dāng)前線程獲得鎖
5. 如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。
6. 如果自旋成功則依然處于輕量級狀態(tài)。
7. 如果自旋失敗,則升級為重量級鎖。

分段鎖

要降低鎖的競爭程度,其中有一種方式是:減少鎖的持有時(shí)間、縮小鎖的范圍、減少鎖的粒度。
這種技術(shù)可以采用多個(gè)相互獨(dú)立的鎖來保護(hù)共享資源來實(shí)現(xiàn),這就是分段鎖。然而這會提高程序的復(fù)雜度,而且使用的鎖越多,發(fā)生死鎖的風(fēng)險(xiǎn)也就越高。但是要做全局的統(tǒng)計(jì)功能時(shí)還是需要對共享資源進(jìn)行全局加鎖。
ConcurrentHashMap中采用了分段鎖。

悲觀鎖和樂觀鎖

悲觀鎖:假定會發(fā)生并發(fā)沖突,屏蔽一切可能違反數(shù)據(jù)完整性的操作,簡單地說是讀寫都加鎖。
樂觀鎖:假定不會發(fā)生并發(fā)沖突,只在提交操作時(shí)檢測是否違反數(shù)據(jù)完整性。(寫時(shí)加鎖,使用版本號或者時(shí)間戳來配合實(shí)現(xiàn),如CAS).

死鎖

死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,他們都將無法推進(jìn)下去。這是一個(gè)嚴(yán)重的問題,因?yàn)樗梨i會讓你的程序掛起無法完成任務(wù),死鎖的發(fā)生必須滿足一下4個(gè)條件:

  • 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用。
  • 請求與保持條件:一個(gè)進(jìn)程因請求資源而阻塞時(shí),對已獲得的資源保持不放。
  • 不剝奪條件:進(jìn)程已獲得的資源,在未使用完之前,不能強(qiáng)行剝奪。
  • 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

避免死鎖最簡單的方法就是破壞循環(huán)等待條件。

活鎖

LiveLock是一種形式活躍性問題,該問題盡管不會阻塞線程,但也不能繼續(xù)執(zhí)行,因?yàn)榫€程將不斷重復(fù)執(zhí)行相同的操作,而且總會失敗。當(dāng)多個(gè)相互協(xié)作的線程都對彼此進(jìn)行響應(yīng)從而修改各自的狀態(tài),并使得任何一個(gè)線程都無法繼續(xù)執(zhí)行時(shí),就發(fā)生了活鎖。這就像兩個(gè)過于禮貌的人在半路上面對面地相遇:他們彼此都給對方讓路,然而又在另一條路上相遇,就這樣反復(fù)里避讓下去,導(dǎo)致誰也過不去。

讀寫鎖、共享鎖和排它鎖、互斥鎖

略。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容