自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

資深架構(gòu)師解讀Java多線程與并發(fā)模型之鎖

原創(chuàng)
開(kāi)發(fā) 后端
這是一篇總結(jié)Java多線程開(kāi)發(fā)的長(zhǎng)文。文章是從Java創(chuàng)建之初就存在的synchronized關(guān)鍵字引入,對(duì)Java多線程和并發(fā)模型進(jìn)行了探討。希望通過(guò)此篇內(nèi)容的解讀能幫助Java開(kāi)發(fā)者更好的理清Java并發(fā)編程的脈絡(luò)。

【51CTO.com原創(chuàng)稿件】互聯(lián)網(wǎng)上充斥著對(duì)Java多線程編程的介紹,每篇文章都從不同的角度介紹并總結(jié)了該領(lǐng)域的內(nèi)容。但大部分文章都沒(méi)有說(shuō)明多線程的實(shí)現(xiàn)本質(zhì),沒(méi)能讓開(kāi)發(fā)者真正“過(guò)癮”。

本篇內(nèi)容從Java的線程安全鼻祖內(nèi)置鎖介紹開(kāi)始,讓你了解內(nèi)置鎖的實(shí)現(xiàn)邏輯和原理以及引發(fā)的性能問(wèn)題,接著說(shuō)明了Java多線程編程中鎖的存在是為了保障共享變量的線程安全使用。下面讓我們進(jìn)入正題。

以下內(nèi)容如無(wú)特殊說(shuō)明均指代Java環(huán)境。

***部分:鎖

提到并發(fā)編程,大多數(shù)Java工程師的***反應(yīng)都是synchronized關(guān)鍵字。這是Java在1.0時(shí)代的產(chǎn)物,至今仍然應(yīng)用于很多的項(xiàng)目中,伴隨著Java的版本更新已經(jīng)存在了20多年。在如此之長(zhǎng)的生命周期中,synchronized內(nèi)部也在進(jìn)行著“自我”進(jìn)化。

早期的synchronized關(guān)鍵字是Java并發(fā)問(wèn)題的唯一解決方案, 伴隨引入這種“重量型”鎖,帶來(lái)的性能開(kāi)銷(xiāo)也是很大的,早期的工程師為了解決性能開(kāi)銷(xiāo)問(wèn)題,想出了很多解決方案(例如DCL)來(lái)提升性能。好在Java1.6提供了鎖的狀態(tài)升級(jí)來(lái)解決這種性能消耗。一般通俗的說(shuō)Java的鎖按照類(lèi)別可以分為類(lèi)鎖和對(duì)象鎖兩種,兩種鎖之間是互不影響的,下面我們一起看下這兩種鎖的具體含義。

類(lèi)鎖和對(duì)象鎖

由于JVM內(nèi)存對(duì)象中需要對(duì)兩種資源進(jìn)行協(xié)同以保證線程安全,JVM堆中的實(shí)例對(duì)象和保存在方法區(qū)中的類(lèi)變量。因此Java的內(nèi)置鎖分為類(lèi)鎖和對(duì)象鎖兩種實(shí)現(xiàn)方式實(shí)現(xiàn)。前面已經(jīng)提到類(lèi)鎖和對(duì)象鎖是相互隔離的兩種鎖,它們之間不存在相互的直接影響,以不同方式實(shí)現(xiàn)對(duì)共享對(duì)象的線程安全訪問(wèn)。下面根據(jù)兩種鎖的隔離方式做如下說(shuō)明:

1、當(dāng)有兩個(gè)(或以上)線程共同去訪問(wèn)一個(gè)Object共享對(duì)象時(shí),同一時(shí)刻只有一個(gè)線程可以訪問(wèn)該對(duì)象的synchronized(this)同步方法(或同步代碼塊),也就是說(shuō),同一時(shí)刻,只能有一個(gè)線程能夠得到CPU的執(zhí)行,另一個(gè)線程必須等待當(dāng)前獲得CPU執(zhí)行的線程完成之后才有機(jī)會(huì)獲取該共享對(duì)象的鎖。

2、當(dāng)一個(gè)線程已經(jīng)獲得該Object對(duì)象的同步方法(或同步代碼塊)的執(zhí)行權(quán)限時(shí),其他的線程仍然可以訪問(wèn)該對(duì)象的非synchronized方法。

3、當(dāng)一個(gè)線程已經(jīng)獲取該Object對(duì)象的synchronized(this)同步方法(或代碼塊)的鎖時(shí),該對(duì)象被類(lèi)鎖修飾的同步方法(或代碼塊)仍然可以被其他線程在同一CPU周期內(nèi)獲取,兩種鎖不存在資源競(jìng)爭(zhēng)情況。

在我們對(duì)內(nèi)置鎖的類(lèi)別有了基本了解后,我們可能會(huì)想JVM是如何實(shí)現(xiàn)和保存內(nèi)置鎖的狀態(tài)的,其實(shí)JVM是將鎖的信息保存在Java對(duì)象的對(duì)象頭中。首先我們看下Java的對(duì)象頭是怎么回事。

Java對(duì)象頭

為了解決早期synchronized關(guān)鍵字帶來(lái)的鎖性能開(kāi)銷(xiāo)問(wèn)題,從Java1.6開(kāi)始引入了鎖狀態(tài)的升級(jí)方式用以減輕1.0時(shí)代鎖帶來(lái)的性能消耗,對(duì)象的鎖由無(wú)鎖狀態(tài) -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖狀的升級(jí)。

 

圖1.1:對(duì)象頭

在Hotspot虛擬機(jī)中對(duì)象頭分為兩個(gè)部分(數(shù)組還要多一部分用于存儲(chǔ)數(shù)組長(zhǎng)度),其中一部分用來(lái)存儲(chǔ)運(yùn)行時(shí)數(shù)據(jù),如HashCode、GC分代信息、鎖標(biāo)志位,這部分內(nèi)容又被稱(chēng)為Mark Word。在虛擬機(jī)運(yùn)行期間,JVM為了節(jié)省存儲(chǔ)成本會(huì)對(duì)Mark Word的存儲(chǔ)區(qū)間進(jìn)行重用,因此Mark Word的信息會(huì)隨著鎖狀態(tài)變化而改變。另外一部分用于方法區(qū)的數(shù)據(jù)類(lèi)型指針存儲(chǔ)。

Java的內(nèi)置鎖的狀態(tài)升級(jí)實(shí)現(xiàn)是通過(guò)替換對(duì)象頭中的Mark Word的標(biāo)識(shí)來(lái)實(shí)現(xiàn)的,下面具體看下內(nèi)置鎖的狀態(tài)是如何從無(wú)鎖狀態(tài)升級(jí)為重量級(jí)鎖狀態(tài)。

內(nèi)置鎖的狀態(tài)升級(jí)

JVM為了提升鎖的性能,共提供了四種量級(jí)的鎖。級(jí)別從低到高分為:無(wú)狀態(tài)的鎖、偏向鎖、輕量級(jí)的鎖和重量級(jí)的鎖。在Java應(yīng)用中加鎖大多使用的是對(duì)象鎖,對(duì)象鎖隨著線程競(jìng)爭(zhēng)的加劇,最終可能會(huì)升級(jí)為重量級(jí)的鎖。鎖可以升級(jí)但不能降級(jí)(也就是為什么我們進(jìn)行任何基準(zhǔn)測(cè)試都需要對(duì)數(shù)據(jù)進(jìn)行預(yù)熱,以防止噪聲的干擾,當(dāng)然噪聲還可能是其他原因)。在說(shuō)明內(nèi)置鎖狀態(tài)升級(jí)之前,先介紹一個(gè)重要的鎖概念,自旋鎖。

自旋鎖

在互斥(mutex)狀態(tài)下的內(nèi)置鎖帶來(lái)的性能下降是很明顯的。沒(méi)有得到鎖的線程需要等待持有鎖的線程釋放鎖才可以爭(zhēng)搶運(yùn)行,掛起和恢復(fù)一個(gè)線程的操作都需要從操作系統(tǒng)的用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài)來(lái)完成。然而CPU為保障每個(gè)線程都能得到運(yùn)行,分配的時(shí)間片是有限的,每次上下文切換都是非常浪費(fèi)CPU的時(shí)間片的,在這種條件下自旋鎖發(fā)揮了優(yōu)勢(shì)。

所謂自旋,就是讓沒(méi)有得到鎖的線程自己運(yùn)行一段時(shí)間,線程自旋是不會(huì)引起線程休眠的(自旋會(huì)一直占用CPU資源),所以并不是真正的阻塞。當(dāng)線程狀態(tài)被其他線程改變才會(huì)進(jìn)入臨界區(qū),進(jìn)而被阻塞。在Java1.6版本已經(jīng)默認(rèn)開(kāi)啟了該設(shè)置(可以通過(guò)JVM參數(shù)-XX:+UseSpinning開(kāi)啟,在Java1.7中自旋鎖的參數(shù)已經(jīng)被取消,不再支持用戶(hù)配置而是虛擬機(jī)總會(huì)默認(rèn)執(zhí)行)。

雖然自旋鎖不會(huì)引起線程的休眠,減少了等待時(shí)間,但自旋鎖也存在著對(duì)CPU資源浪費(fèi)的情況,自旋鎖需要在運(yùn)行期間空轉(zhuǎn)CPU的資源。只有當(dāng)自旋等待的時(shí)間高于同步阻塞時(shí)才有意義。因此JVM限制了自旋的時(shí)間限度,當(dāng)超過(guò)這個(gè)限度時(shí),線程就會(huì)被掛起。

在Java1.6 中提供了自適應(yīng)自旋鎖,優(yōu)化了原自旋鎖限度的次數(shù)問(wèn)題,改為由自旋線程時(shí)間和鎖的狀態(tài)來(lái)確定。例如,如果一個(gè)線程剛剛自旋成功獲取到鎖,那么下次獲取鎖的可能性就會(huì)很大,所以JVM準(zhǔn)許自旋的時(shí)間相對(duì)較長(zhǎng),反之,自旋的時(shí)間就會(huì)很短或者忽略自旋過(guò)程,這種情況在Java1.7也得到了優(yōu)化。

自旋鎖是貫穿內(nèi)置鎖狀態(tài)始終的,作為偏向鎖,輕量級(jí)鎖以及重量級(jí)鎖的補(bǔ)充。

偏向鎖

偏向鎖是Java1.6 提出的一種鎖優(yōu)化機(jī)制,其核心思想是,如果當(dāng)前線程沒(méi)有競(jìng)爭(zhēng)則取消之前已經(jīng)取得鎖的線程同步操作,在JVM的虛擬機(jī)模型中減少對(duì)鎖的檢測(cè)。也就是說(shuō)如果某個(gè)線程取得對(duì)象的偏向鎖,那么當(dāng)這個(gè)線程在此請(qǐng)求該偏向鎖時(shí),就不需要額外的同步操作了。

具體的實(shí)現(xiàn)為當(dāng)一個(gè)線程訪問(wèn)同步塊時(shí)會(huì)在對(duì)象頭的Mark Word中存儲(chǔ)鎖的偏向線程ID,后續(xù)該線程訪問(wèn)該鎖時(shí),就可以簡(jiǎn)單的檢查下Mark Word是否為偏向鎖并且其偏向鎖是否指向當(dāng)前線程。

如果測(cè)試成功則線程獲取到偏向鎖,如果測(cè)試失敗,則需要檢測(cè)下Mark Word中偏向鎖的標(biāo)記是否設(shè)置成了偏向狀態(tài)(標(biāo)記位為1)。如果沒(méi)有設(shè)置,則使用CAS競(jìng)爭(zhēng)鎖。如果設(shè)置了,嘗試使用CAS將對(duì)象頭的Mark Word偏向鎖標(biāo)記指向當(dāng)前線程。也可以使用JVM參數(shù)-XX:-UseBiastedLocking參數(shù)來(lái)禁用偏向鎖。

因?yàn)槠蜴i使用的是存在競(jìng)爭(zhēng)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。

輕量級(jí)的鎖

如果偏向鎖獲取失敗,那么JVM會(huì)嘗試使用輕量級(jí)鎖,帶來(lái)一次鎖的升級(jí)。輕量級(jí)鎖存在的出發(fā)點(diǎn)是為了優(yōu)化鎖的獲取方式,在不存在多線程競(jìng)爭(zhēng)的前提下,以減少Java 1.0時(shí)代鎖互斥帶來(lái)的性能開(kāi)銷(xiāo)。輕量級(jí)鎖在JVM內(nèi)部是使用BasicObjectLock對(duì)象實(shí)現(xiàn)的。

其具體的實(shí)現(xiàn)為當(dāng)前線程在進(jìn)入同步代碼塊之前,會(huì)將BasicObjectLock對(duì)象放到Java的棧楨中,這個(gè)對(duì)象的內(nèi)部是由BasicLock對(duì)象和該Java對(duì)象的指針組成的。然后當(dāng)前線程嘗試使用CAS替換對(duì)象頭中的Mark Word鎖標(biāo)記指向該鎖記錄指針。如果成功則獲取到鎖,將對(duì)象的鎖標(biāo)記改為00 | locked,如果失敗則表示存在其他線程競(jìng)爭(zhēng),當(dāng)前線程使用自旋嘗試獲取鎖。

當(dāng)存在兩條(或以上)的線程共同競(jìng)爭(zhēng)一個(gè)鎖時(shí),此時(shí)的輕量級(jí)的鎖將不再發(fā)揮作用,JVM會(huì)將其膨脹為重量級(jí)的鎖,鎖的標(biāo)位為也會(huì)修改為10 | monitor 。

輕量級(jí)鎖在解鎖時(shí),同樣是通過(guò)CAS的置換對(duì)象頭操作。如果成功,則表示成功獲取到鎖。如果失敗,則說(shuō)明該對(duì)象存在其他線程競(jìng)爭(zhēng),該鎖會(huì)隨著膨脹為重量級(jí)的鎖。

重量級(jí)的鎖

JVM在輕量級(jí)鎖獲取失敗后,會(huì)使用重量級(jí)的鎖來(lái)處理同步操作,此時(shí)對(duì)象的Mark Word標(biāo)記為 10 | monitor,在重量級(jí)鎖處理線程的調(diào)度中,被阻塞的線程會(huì)被系統(tǒng)掛起,在線程再次獲得CPU資源后,需要進(jìn)行系統(tǒng)上下文的切換才能得到CPU執(zhí)行,此時(shí)效率會(huì)低很多。

通過(guò)上面的介紹我們了解了Java的內(nèi)置鎖升級(jí)策略,隨著鎖的每次升級(jí)帶來(lái)的性能的下降,因此我們?cè)诔绦蛟O(shè)計(jì)時(shí)應(yīng)該盡量避免鎖的征用,可以使用集中式緩存來(lái)解決該問(wèn)題。

一個(gè)小插曲:內(nèi)置鎖的繼承

內(nèi)置鎖是可以被繼承的,Java的內(nèi)置鎖在子類(lèi)對(duì)父類(lèi)同步方法進(jìn)行方法覆蓋時(shí),其同步標(biāo)志是可以被子類(lèi)繼承使用的,我們看下面的例子: 

  1. public class Parent { 
  2. public synchronized void doSomething() { 
  3.      System.out.println("parent do something"); 
  4.  
  5. public class Child extends Parent { 
  6. public synchronized void doSomething() { 
  7. .doSomething(); 
  8.  
  9. public static void main(String[] args) { 
  10.      new Child().doSomething(); 
  11.  

代碼1.1:內(nèi)置鎖繼承

以上的代碼可以正常的運(yùn)行么?

答案是肯定的。

避免活躍度危險(xiǎn)

Java并發(fā)的安全性和活躍度是相互影響的,我們使用鎖來(lái)保障線程安全的同時(shí),需要避免線程活躍度的風(fēng)險(xiǎn)。Java線程不能像數(shù)據(jù)庫(kù)那樣自動(dòng)排查解除死鎖,也無(wú)法從死鎖中恢復(fù)。而且程序中死鎖的檢查有時(shí)候并不是顯而易見(jiàn)的,必須到達(dá)相應(yīng)的并發(fā)狀態(tài)才會(huì)發(fā)生,這個(gè)問(wèn)題往往給應(yīng)用程序帶來(lái)災(zāi)難性的結(jié)果,這里介紹以下幾種活躍度危險(xiǎn):死鎖、線程饑餓、弱響應(yīng)性、活鎖。

死鎖

當(dāng)一個(gè)線程永遠(yuǎn)的占有一個(gè)鎖,而其他的線程嘗試去獲取這個(gè)鎖時(shí),這個(gè)線程將被***的阻塞。

一個(gè)經(jīng)典的例子就是AB鎖問(wèn)題,線程1獲取到了共享數(shù)據(jù)A的鎖,同時(shí)線程2獲取到了共享數(shù)據(jù)B的鎖,此時(shí)線程1想要去獲取共享數(shù)據(jù)B的鎖,線程2獲取共享數(shù)據(jù)A的鎖。如果用圖的關(guān)系表示,那么這將是一個(gè)環(huán)路。這是死鎖是最簡(jiǎn)單的形式。還有比如我們?cè)賹?duì)批量無(wú)序的數(shù)據(jù)做更新操作時(shí),如果無(wú)序的行為引發(fā)了2個(gè)線程的資源爭(zhēng)搶也會(huì)引發(fā)該問(wèn)題,解決的途徑就是排序后再進(jìn)行處理。

線程饑餓

線程饑餓是指當(dāng)線程訪問(wèn)它所需要的資源時(shí)卻***被拒絕,以至于不能再繼續(xù)進(jìn)行后面的流程,這樣就發(fā)生了線程饑餓;例如線程對(duì)CPU時(shí)間片的競(jìng)爭(zhēng),Java中低優(yōu)先級(jí)的線程引用不當(dāng)?shù)?。雖然Java的API中對(duì)線程的優(yōu)先級(jí)進(jìn)行了定義,這僅僅是一種向CPU自我推薦的行為(此處需要注意不同操作系統(tǒng)的線程優(yōu)先級(jí)并不統(tǒng)一,而且對(duì)應(yīng)的Java線程優(yōu)先級(jí)也不統(tǒng)一),但是這并不能保障高優(yōu)先級(jí)的線程一定能夠先被CPU選擇執(zhí)行。

弱響應(yīng)性

在GUI的程序中,我們一般可見(jiàn)的客戶(hù)端程序都是使用后臺(tái)運(yùn)行,前端反饋的形式,當(dāng)CPU密集型后臺(tái)任務(wù)與前臺(tái)任務(wù)共同競(jìng)爭(zhēng)資源時(shí),有可能造成前端GUI凍結(jié)的效果,因此我們可以降低后臺(tái)程序的優(yōu)先級(jí),盡可能的保障***的用戶(hù)體驗(yàn)性。

活鎖

線程活躍度失敗的另一種體現(xiàn)是線程沒(méi)有被阻塞,但是卻不能繼續(xù),因?yàn)椴粩嘀卦囅嗤牟僮鳎瑓s總是失敗。

線程的活躍度危險(xiǎn)是我們?cè)陂_(kāi)發(fā)中應(yīng)該避免的一種行為。這種行為會(huì)造成應(yīng)用程序的災(zāi)難性后果。

總結(jié)

關(guān)于synchronized關(guān)鍵字的所有內(nèi)容到這里全部介紹完畢了,在這一章節(jié)希望可以讓大家明白鎖之所以“重”是因?yàn)殡S著線程間競(jìng)爭(zhēng)的程度升級(jí)導(dǎo)致的。在真正的開(kāi)發(fā)中我們可能還有別的選擇,例如Lock接口,在某些并發(fā)場(chǎng)景下性能優(yōu)于內(nèi)置鎖的實(shí)現(xiàn)。

不論是通過(guò)內(nèi)置鎖還是通過(guò)Lock接口都是為了保障并發(fā)的安全性,并發(fā)環(huán)境一般需要考慮的問(wèn)題是如何保障共享對(duì)象的安全訪問(wèn)。在第二章將詳細(xì)介紹內(nèi)置對(duì)象引發(fā)的線程安全問(wèn)題以及解決之道。  

作者簡(jiǎn)介

魏靚:現(xiàn)就職于五阿哥(www.wuage.com)任職專(zhuān)職架構(gòu)師工作,負(fù)責(zé)平臺(tái)的基礎(chǔ)設(shè)施搭建工作。

【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】

責(zé)任編輯:龐桂玉 來(lái)源: 51CTO.com
相關(guān)推薦

2017-11-22 09:00:00

2018-07-03 15:46:24

Java架構(gòu)師源碼

2023-10-08 09:34:11

Java編程

2017-09-16 18:29:00

代碼數(shù)據(jù)庫(kù)線程

2012-11-01 15:08:10

IBM資深架構(gòu)師

2017-05-31 14:03:07

Java多線程內(nèi)置鎖與顯示鎖

2017-05-08 11:46:15

Java多線程

2018-02-05 09:30:23

高性能高并發(fā)服務(wù)

2018-09-13 15:00:51

JavaHashMap架構(gòu)師

2021-07-19 07:55:24

多線程模型Redis

2009-12-08 10:07:29

2022-06-15 07:32:35

Lock線程Java

2023-06-09 07:59:37

多線程編程鎖機(jī)制

2010-05-04 08:44:42

Java并發(fā)模型

2013-10-17 15:45:24

紅帽

2013-10-17 15:54:46

紅帽

2015-04-10 17:35:26

WOT2015谷歌資深架構(gòu)師李聰

2021-06-07 09:35:11

架構(gòu)運(yùn)維技術(shù)

2022-05-26 08:31:41

線程Java線程與進(jìn)程

2019-10-21 09:32:48

緩存架構(gòu)分層
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)