JVM中的鎖

什么是線程安全的?

在Java 并發(fā)編程實戰(zhàn)中 ,當多個線程同時訪問同一個對象的時候,如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進行額外的同步,或者在調(diào)用方法進行任何其他的協(xié)調(diào)操作,調(diào)用這個對象都會產(chǎn)生正確的結果,那就稱這個對象是線程安全的。

就是一個對象在多線程的環(huán)境中運行,該對象總會產(chǎn)生正確的結果。

線程對立是產(chǎn)生死鎖的條件,在Java 中 ,所以就盡量少使用線程對立的方法,比如Thread 類的suspend方法和resume 方法,線程中斷和線程恢復,這兩個方法已經(jīng)被廢棄

實現(xiàn)線程安全的幾種方法

對象的絕對不可變,對象被創(chuàng)建之后,任何屬性都不可以被修改,而且內(nèi)部的任何引用不可泄漏或者任何引用都是不可變的。也就是說類 以及類內(nèi)部的所有屬性都是被final 關鍵字修飾,另外封裝外面的數(shù)據(jù)任何情況都不可以觸摸到里面的數(shù)據(jù),而類內(nèi)部的外部引用也必須是絕對不可變的,拿 String 類來舉例子,String 類內(nèi)部的所有屬性在初始化之后就不可以被修改了,每天添加其實都是重新創(chuàng)建了的一個新的String 對象,在Java 中這樣的不可變類,有String? Number 的 一些子類 ,因為他們都采用了常量池技術,所有必須設置為不可變類,

互斥同步 ( 阻塞同步 ) 屬于悲觀鎖

公共數(shù)據(jù)每次只能被一個線程訪問

最基本步驟就是synchronized 關鍵字,這是一個重量級鎖。在多線程編程的過程中盡量少用這種方式。然后就是重入鎖ReentrantLock ,該鎖用的更加靈活,等待可中斷,( 當該線程等待獲取鎖的時候,可以設置線程等待時間,如果在規(guī)定時間內(nèi)沒有獲得鎖,則直接放棄這次操作。)公平鎖,多個線程在獲取同一個鎖的時候,獲取鎖的先后順序是根據(jù)等待時間長短依次獲取鎖。鎖綁定多個條件

互斥同步對性能最大的影響是阻塞的實現(xiàn),掛起線程和恢復線程的造作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這是很慢的。而且共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短時間,

非阻塞同步 采用樂觀鎖的思想

互斥同步的思想有很大的優(yōu)化空間,因為在大多數(shù)情況下,共享數(shù)據(jù)都不會出現(xiàn)競爭,但是都會對其進行鎖,這就浪費了大量的時間和資源。這是悲觀鎖的設計思路。

我們可以采用樂觀鎖的設計思想來設計,也就是先不管風險,先進行操作,如果沒有其他線程進行操作,那么操作成功了,如果共享資源被占用了,再進行補償?shù)拇胧畛S玫难a償就是不斷地重試,直到?jīng)]有競爭為止。但是這種操作需要硬件支持的原子性,也就是一條處理器指令就可以完成的

這類的指令有:

測試并設置

獲取并增加

交換

比較并交換CAS

加載鏈接/條件儲存

這就和我們經(jīng)常談到的CAS 算法有關了,CAS 算法有三個量, V A B 這里,其中 V 是要修改數(shù)據(jù)在內(nèi)存中的地址 ,A 是舊的數(shù)據(jù),B 是新的數(shù)據(jù),CAS 算法是通過CAS 指令,當且僅當A 符合 V 時,處理器才會更新B 值,注意CAS 指令是一個機器指令,也就是說是一個原子操作,這是CAS真正的解釋,當然其他的機器指令也可能會有相應的同步算法,Java 中大多數(shù)的原子類和原子操作都用到了CAS 算法,我們以Integer 的原子類舉個例子,來判斷是否會發(fā)生同步問題

// 這里我測試了好多此,利用二十個線程去搶這個資源,是不會發(fā)送線程同步問題的

staticAtomicIntegeract=newAtomicInteger(0);

publicstaticvoidmain(String[]args)throwsExecutionException,InterruptedException{

?

Runnablerunnable=newRunnable() {

@Override

publicvoidrun() {

for(inti=0;i<100;i++){

act.incrementAndGet();

System.out.println(act);

? ? ? ? ? ? ?? }

? ? ? ? ?? }

? ? ?? } ;

ExecutorServiceexecutorService=Executors.newFixedThreadPool(200);

for(inti=0;i<200;i++){

executorService.submit(runnable);

? ? ?? }

executorService.shutdown();

?? }

?

// 輸出

19994

19995

19996

19997

19998

19999

20000

// 我們換成`Integr` 試一下

staticintact=0;

publicstaticvoidmain(String[]args)throwsExecutionException,InterruptedException{

?

Runnablerunnable=newRunnable() {

@Override

publicvoidrun() {

for(inti=0;i<100;i++){

act++;

System.out.println(act);

? ? ? ? ? ? ?? }

? ? ? ? ?? }

? ? ?? } ;

ExecutorServiceexecutorService=Executors.newFixedThreadPool(200);

for(inti=0;i<200;i++){

executorService.submit(runnable);

? ? ?? }

executorService.shutdown();

?? }

19993

19994

19995

19996

19997

19998

?

我們可以看一下AtomicInteger 類中 increamentAndGet 方法的源碼

publicfinalintincrementAndGet() {

returnunsafe.getAndAddInt(this,valueOffset,1)+1;

}

publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4) {

intvar5;

do{

var5=this.getIntVolatile(var1,var2);

}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));

?

returnvar5;

}

?

// 這里可以看到底層是一個循環(huán),直到值相加成功之后才會退出

// 該方法中調(diào)用的方法是全部使用的 JVM 實現(xiàn)的 在類中明確標注,這些方法沒有沒有實現(xiàn)的代碼,均為JVM 底層采用 CAS機器指令 支持實現(xiàn)的

CAS算法的漏洞,這個和CAS 指令沒有關系,

CAS 的實質(zhì)是Compare And Swap 那么只要在比較的時候符合要求就可以了,所以這樣就可以想到,當一個線程對共享數(shù)據(jù)加時,另一個線程對共享數(shù)據(jù)減的時候,在比較的時候,或者恰好是相當于沒有進行變化的值,那么就直接騙過Compare 了 ,這就是CAS 算法的ABA 問題,對該類的處理方式,大部分的ABA 問題都不會程序并發(fā)的特性,如果需要解決該類問題,直接使用傳統(tǒng)的同步鎖就可以了

無同步方案

在高并發(fā)編程中,如果讓一個方法不涉及共享數(shù)據(jù),那它自然就不需要任何同步措施保證正確性。

可重入代碼( 純代碼 ),特指該段代碼在任何時候被中斷,轉(zhuǎn)而執(zhí)行另一段代碼( 包括遞歸調(diào)用本身 ),在控制權返回后,原來的程序不會出現(xiàn)任何錯誤,也不會對結果有影響。所有可重入代碼都是線程安全的,反之不一定。

可重入代碼的一些公共特性,不依賴全局變量,存儲在堆上的數(shù)據(jù)和公用的系統(tǒng)資源都由參數(shù)傳入,不調(diào)用不可重入的方法。另一種方式來說,就是說該方法的執(zhí)行結果是可預測的,只要輸入了相同的數(shù)據(jù),就會返回相同的結果,平時刷的算法題都屬于可重入代碼。

線程本地存儲,如果一段代碼中所需要的數(shù)據(jù)必須與其他的代碼共享,我們試一下,可以將這些共享數(shù)據(jù)的代碼放在同一個線程中執(zhí)行。如果可以,不需要線程同步也可以保證,線程之間不存在數(shù)據(jù)重用的概念。如果不可以,盡量使用少的線程,這樣也可以節(jié)省計算機的系統(tǒng)資源。

這種例子有很多,一個簡單的生產(chǎn)者消費者例子中,如果將生產(chǎn)者和消費者都放在同一個線程中,這個多線程問題就變成了單線程的問題,完全不需要考慮并發(fā)的問題。另如,經(jīng)典Web 交換模型,一個請求對應著一個服務器線程,這些線程之間幾乎不會考慮到線程安全的問題。

這里就需要提到volatile 關鍵字和ThreadLoacl類來實現(xiàn)線程本地存儲。

volatile 關鍵字是將對象聲明為易變的,這樣對象在在變化時,其他線程才能感知到。而ThreadLoacl 類中,數(shù)據(jù)是以鍵值對的形式來存儲數(shù)據(jù),每一個線程對應著一個對象,這個存儲技術與HashMap 技術是類似的

鎖優(yōu)化

自旋鎖與自適應自選

前面我們提到了同步互斥的影響并發(fā)效率的主要原因是要將線程掛起和恢復,這些操作都是需要轉(zhuǎn)入內(nèi)核態(tài)中完成,運行時間比較長,但是其實共享數(shù)據(jù)被鎖住的時間是很短的,為了這短暫的鎖定時間,更付出將線程掛機和恢復的代價,是非常不值的。所有就有了自旋鎖,當一個線程要訪問共享資源的時候,發(fā)現(xiàn)共享資源被鎖住,自旋鎖不會將線程掛起,而是會在原地進行自選,直到共享資源被釋放。這樣就沒有了線程的掛起和恢復的過程,這樣鎖的效率就會高上很多。但是同時也會出現(xiàn)問題,如果共享資源被占用時間比較短,這個工作效率就比較高,如果占用時間非常長呢!那么自旋線程的時間就會很長,這樣就會浪費大量的系統(tǒng)資源,所以又產(chǎn)生了一個新的自旋鎖優(yōu)化,也就是自適應自旋鎖,這種鎖會預測公共資源占用的時間,若公共變量占用的時間非常長的話,就不會進行自選,還是采用老方法,線程掛起。

鎖消除

鎖消除的概念就是指虛擬機即時編譯器在運行時,對一些代碼要求同步,對被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除,

這個判斷的依據(jù)是根據(jù)逃逸分析的數(shù)據(jù)支持。

鎖粗化

我們在編寫代碼的過程,總是推薦將同步代碼塊的范圍設置的盡量小,因為這樣鎖持有的時間是很短的,很快就會將鎖釋放,在一般情況下,這種設計是沒有任何問題的,但是如果一個系統(tǒng)變量,一直被加鎖,一直被解鎖,這樣的話,在整個過程中,大部分的時間都會被浪費在線程掛起和線程釋放的這個過程,所以這時可以將鎖的作用范圍加大,覆蓋整個加鎖解鎖的過程,這樣就不會一直加鎖解鎖,只需要一次就可以了,這樣提升了很多效率,但是這只是在很少情況下,才會使用的。

輕量級鎖

在java 虛擬機內(nèi)部,每個對象都是有一個對象頭的Mark work ,在不同的對象狀態(tài),對象頭存儲的內(nèi)容是不同的,而對象的狀態(tài)大概分為五種,

未鎖定狀態(tài)

輕量級鎖定狀態(tài)

重量級鎖定(鎖膨脹)狀態(tài)

GC 標記狀態(tài)

可偏向狀態(tài)

在代碼即將進入同步塊的時候,如果此同步對象沒有被鎖定,虛擬機首先在當前線程棧幀中建立一個名為鎖記錄的空間來存儲鎖對象當前的對象頭的拷貝。然后虛擬機通過CAS 操作嘗試把對象的對象頭更新為指向棧幀中對象頭的拷貝的指針,如果成功了,該線程就擁有了這個對象的鎖,鎖狀態(tài)就變成了輕量級鎖定狀態(tài),此時的對象頭存儲的是在棧楨中的地址。如果CAS 操作失敗了,說明至少存在一條其他線程也在搶這個對象的鎖,虛擬機會根據(jù)該對象的對象頭來判斷,若對象都指向的是當前線程的棧幀,那么當前線程成功搶到鎖了,反之就沒有,如果出現(xiàn)兩個以上的線程搶這個鎖,此時輕量級鎖就沒有用了,必須膨脹為重量級鎖,其他線程就必須進入阻塞狀態(tài)。這是加鎖的過程,解鎖的過程也是采用CAS 操作來執(zhí)行的,將棧幀中的內(nèi)容與對象頭的內(nèi)容呼喚,若互換失敗,說明其他線程也在搶這個鎖,那么在釋放鎖的過程中還會喚醒其他的掛起的線程。

在絕對多數(shù)情況下,同步的代碼是不存在鎖競爭的,也就是極少情況下,才會出現(xiàn) 多個線程同時搶同一個共享對象的情況,這是輕量級鎖提升同步性能的原因,如果在多個線程同時搶一個共享對象時,那么輕量級鎖與重量級鎖相比,會更慢,因為過程會執(zhí)行CAS 操作。

偏向鎖

偏向鎖的設計其實與輕量級鎖類似,偏向鎖是為消除數(shù)據(jù)在無競爭條件下的同步,進一步提升程序的運行性能。

根據(jù)字面意思,偏向鎖就是偏向的鎖,更偏向第一個獲取鎖的線程,當啟用偏向時,當對象被第一個線程獲取時,對象頭會進入可偏向狀態(tài),對象頭會記錄線程的ID,這個操作也是采用CAS ,當該線程下一次再來訪問,不需要搶鎖,加鎖解鎖,相當于該對象是屬于該線程的,此時如果出現(xiàn)另一個線程來訪問該對象,那么偏向模式立馬結束,然后進入輕量級鎖的過程,

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

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

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