從零單排Java多線程(2)

Java線程同步機制

鎖概述

臨界區(qū):鎖獲得與鎖釋放之間執(zhí)行的代碼稱為臨界區(qū)

一個鎖一次只能被一個線程持有,稱為排他鎖或者互斥鎖(Mutex)

java平臺中的鎖包含內(nèi)部鎖(通過關(guān)鍵字synchronized)和顯示鎖(通過java.concurrent.locks.Lock接口的實現(xiàn)類)

鎖是如何保證線程安全?

原子性

通過互斥保障,程序在執(zhí)行臨界區(qū)間沒有其他線程能夠訪問相應的共享變量(要是synchronized,就是synchronized的鎖句柄)

可見性

鎖的獲得隱含著刷新處理器緩存這個動作,鎖的釋放隱含著沖刷處理器緩存這個操作,所以可以保證可見性

鎖的互斥性和可見性,可以保證臨界區(qū)內(nèi)的代碼能夠讀取到共享數(shù)據(jù)的最新值。(讀取這個共享變量的線程在讀取并使用該變量時,其他線程無法更新該變量的值)

有序性

原子性+可見性使得操作線程對其他線程來說,這些共享變量好像是同一時刻更新的,并不用關(guān)心操作線程是以什么順序來更新變量。

要保證線程安全需要滿足以下兩點:

  1. 線程訪問同一組共享數(shù)據(jù)的時候必須使用同一個鎖
  2. 任意一個線程,即使僅僅是讀取這組共享數(shù)據(jù),沒有對其進行更新的話,也需要在讀取的時候持有相應的鎖

鎖的幾個概念

可重入鎖

一個線程在持有一個鎖的時候能否再次(或多次)申請該鎖。如果一個線程持有一個鎖的時候還能夠繼續(xù)成功申請該鎖,那么我們就稱該鎖是可重入的??芍厝腈i可以用一個計數(shù)器屬性來實現(xiàn),獲得鎖+1,釋放鎖-1;可重入鎖使得持有鎖的線程再次后的鎖的開銷很小。

鎖泄露

一個線程獲得某個鎖之后,由于程序錯誤,該鎖一直無法被釋放,其他線程無法獲得該鎖。一般出現(xiàn)在顯示鎖中,未正確處理異常。

內(nèi)部鎖:synchronized 關(guān)鍵字

synchronized修飾方法以及代碼塊。

修飾代碼塊:synchronized(鎖句柄){
    //在此代碼塊中訪問共享數(shù)據(jù);
}

書上的描述

習慣上我們直接稱鎖句柄為鎖,鎖句柄對應的監(jiān)視器稱為相應同步塊的引導鎖,線程在執(zhí)行臨界區(qū)代碼的時候必須持有該臨界區(qū)的引導鎖。如果這個線程沒有執(zhí)行臨界區(qū)代碼,則不需要持有,需要注意的是,synchronized鎖住的是對象,但是只有執(zhí)行臨界區(qū)代碼才需要去獲得這個引導鎖。

這里需要理解什么是監(jiān)視器:監(jiān)視器其實是一種同步機制,Java中每個對象都有一個監(jiān)視器與之關(guān)聯(lián),利用這個來實現(xiàn)同步。jvm實現(xiàn)synchronized時實際上是在調(diào)用同步方法之前調(diào)用Monitor.enter,結(jié)束之后調(diào)用Monitor.exit;

繪圖1.jpg

內(nèi)部鎖不會導致鎖泄露,javac對臨界區(qū)可能拋出又未捕獲的異常進行了處理

顯式鎖:LOCK

顯示鎖是java.util.concurrent.locks.Lock接口的實例,默認實現(xiàn)是ReentrantLock

private final Lock lock=...;//創(chuàng)建一個顯示鎖
...
lock.lock();//申請鎖lock
try{
    //在此對共享數(shù)據(jù)處理
}fianlly{
    lock.unlock();//這里注意:必須在finally中釋放鎖,否則可能導致鎖泄露
}

使用方法大概與synichronized一樣。

顯式鎖與內(nèi)部鎖的對比

  1. 內(nèi)部鎖基于代碼塊的鎖,使用比較不靈活,而顯式鎖是基于對象的鎖,較為靈活。
  2. 調(diào)度方面,內(nèi)部鎖只支持非公平,顯式鎖支持公平和非公平
  3. 如果一個內(nèi)部鎖持有線程一直不釋放這個鎖,所有同步在該鎖上的所有其他線程就會一直被暫停使其任務無法進行,但是顯式鎖可以用lock.tryLock()來嘗試獲取鎖,而不堵塞。
  4. 性能方面:內(nèi)部鎖做了優(yōu)化,在特定情況下可以減少鎖的開銷:包括鎖消除(Lock Elimination)、偏向鎖(Biased Lock)和適配鎖(Adaptive Lock)。

讀寫鎖

讀寫鎖允許多個線程可以同時讀?。ㄖ蛔x)共享變量,但是一次只允許一個線程對共享變量進行更新,讀取共享變量的時候無法更新這些變量,更新變量的時候其他線程無法讀取。

讀寫鎖的功能實現(xiàn)

讀寫鎖的功能是通過其==扮演的兩種角色==讀鎖(Read Lock)和寫鎖(Write Lock)實現(xiàn)的。

讀寫鎖的兩種角色

獲得條件 排他性 作用
讀鎖 相應的寫鎖未被其他任何線程持有 對讀線程是共享的,對寫線程排他 允許多個讀線程同時讀取共享變量,并保障讀線程讀取共享變量期間沒有其他任何線程能夠更新這些共享變量
寫鎖 寫鎖未被其他任何線程持有,相應的讀鎖未被其他任何線程持有 對讀線程和寫線程都是排他的 使得寫線程能夠以獨占的方式訪問共享變量

java平臺的讀寫鎖實現(xiàn)

java.util.concurrent.locks.ReadWriteLock 接口,默認實現(xiàn)是java.util.concurrent.locks.ReentrantReadWriteLock。ReadWriteLock接口定義了兩個方法:readLock()和writeLock(),分別用于返回相應讀寫鎖的實例

注意事項:并不是一個ReadWriteLock的實例對應兩個鎖,而是一個ReadWriteLock接口實例可以扮演兩個角色

//使用示例,另外ReentrantReadWriteLock是重入鎖,支持鎖的降級,即在持有寫鎖的時候,可以申請讀鎖。但是
//不支持鎖的升級
public class ReadWriteLockExample{
    private final ReadWriteLock rwLock=new ReentrantReadWriteLock();
    private final Lock readLock=rwLock.readLock();
    private final Lock writeLock=rwLock.writeLock();
    public void readExample(){
        readLock.lock();
        try {
            //讀取共享變量
        } finally{
            readLock.unlock();
        }
    }
    public void writeExample(){
        writeLock.lock();
        try {
            //讀取,更新共享變量
        } finally{
            writeLock.unlock();
        }
    }

}

讀寫鎖的選用

讀寫鎖的內(nèi)部實現(xiàn)比其他顯式鎖復雜很多,所以讀寫鎖適合于

  1. 只讀操作比寫操作要頻繁得多
  2. 讀線程持有鎖的時間比較長

只有同時滿足上面兩個條件時,讀寫鎖才是合適的選擇,否則,使用讀寫鎖就會得不償失。

內(nèi)存屏障

這里的內(nèi)存屏障其實防止的是重排序,但是這里的重排序并不是對指令來說,而是對處理器對內(nèi)存的操作。即讀內(nèi)存Load以及寫內(nèi)存Store操作。

如x86匯編代碼:
mov edx,0f80f802ch;//將內(nèi)存0f80f802ch中的內(nèi)容讀入edx
mov 0f80f802ch,edx;//將寄存器edx的內(nèi)容存放到內(nèi)存中

按照可見性保障劃分:

加載屏障(Load Barrier),存儲屏障(Store Barrier).加載屏障的作用是刷新處理器緩存,存儲屏障的作用是沖刷處理器緩存

按照有序性保障劃分:

獲取屏障(Acquire Barrier)進行后續(xù)操作之前必須要先獲取數(shù)據(jù),禁止讀操作與后面的操作重排序,釋放屏障(Release Barrier)寫操作之前插入,禁止寫操作中與前面操作重排序。

MonitorEnter
Load Barrier;
Acquire Barrier
臨界區(qū)
Release Barrier
MonitorExit
Store Barrier

Volatile關(guān)鍵字

volatile關(guān)鍵字用于修飾共享可變變量,沒有用final修飾的共享變量或者靜態(tài)變量。volatile變量不會被編譯器分配到寄存器進行存儲,讀寫操作都是內(nèi)存訪問操作。

volatile的作用

保障可見性

保障有序性

保障long/double型變量讀寫操作的原子性

對volatile變量的賦值操作,其右邊表達式中只要涉及共享變量(包括volatile變量本身),那么這個賦值操作就不是原子操作。

對于volatile變量的寫操作,Java虛擬機會在該操作之前插入一個釋放屏障,并在操作之后插入一個存儲保障。

//寫的時候前面的操作防止重排序,寫后保持可見性
sharedA=1;
Realease Barrier;//禁止寫操作與前面的任何讀寫操作重排序
volatileVar=true;
Store Barrier;//沖刷處理器緩存

對于volatile變量的讀操作

//讀前保證可見性,讀后防止重排序
Load barrier;刷新處理器緩存
localVar=volatileVar;//讀變量操作
Acquire Barrier;//禁止讀操作與之后的操作重排序
localA=sharedA;//普通變量的讀寫操作

注意點:

如果被修飾的變量是一個數(shù)組,volatile只能保證對數(shù)組引用本身的操作(讀取數(shù)組引用,更新數(shù)組引用)起作用,同樣地,對于引用型volatile變量,volatile關(guān)鍵字只能保證線程讀取到一個指向?qū)ο蟮南鄬π碌膬?nèi)存地址。

volatile不會導致上下文切換,所以開銷要比鎖小一點。

volatile的典型應用場景

  1. 使用volatile變量作為狀態(tài)標志。也就是說其中一個線程來管理這個變量,其他線程讀取變量作為計算的依據(jù)(不進行寫操作),這樣只要這個線程只采用寫操作(可以是一個賦值,右邊可以是自己,但不能是其他共享變量(保證其他線程對這個變量只有讀操作)),就可以保證安全。
  2. 多個線程共享一組可變狀態(tài)變量的時候,通常我們需要用鎖來保證這些變量的更新操作的原子性。
  3. 使用volatile代替鎖,多個線程共享一組可變狀態(tài)變量的時候,我們需要使用鎖來保障對這些變量的更新操作的原子性。而我們可以把這些變量封裝成一個對象,那么對這些狀態(tài)變量的更新操作就可以通過創(chuàng)建一個新對象并將該對象的引用賦值給響應的引用型變量。(原則:只寫或只讀,而不讀后寫)
  4. 實現(xiàn)簡易的讀寫鎖。變量聲明為volatile。寫操作聲明為synichronized,寫線程就可以互斥寫,但是讀線程可能讀到的共享變量不是最新值。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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