第三章 java線程同步機制 《java多線程編程實戰指南-核心篇》

3.1 線程同步機制簡介

線程同步機制是一套用於協調線程間的數據訪問及活動的機制,該機制用於保障線程安全以及實現這些線程的共同目標。

線程同步機制包括鎖、volatile關鍵字、final關鍵字、static關鍵字以及相關API。

3.2 鎖概述

獲得鎖(Acquire)、釋放鎖(Release)

鎖的持有線程在其獲得鎖之後和釋放鎖之前這段時間內所執行的代碼被稱爲臨界區。

鎖有排他鎖(互斥鎖)和讀寫鎖。

java中的鎖實現包括內部鎖(synchronized)和顯式鎖(java.concurrent.locks.Lock的實現類)

鎖能夠保護共享數據以實現線程安全,其作用包括保障原子性、可見性和有序性

  • 鎖通過互斥保障原子性;
  • 可見性的保障是通過寫線程沖刷處理器緩存和讀線程刷新處理器緩存這兩個動作實現的。在java平臺中,鎖的獲得隱含着刷新處理器緩存這個動作,這使得讀線程在執行臨界區代碼前可以將寫線程對共享變量所做的更新同步到該線程執行處理器的高速緩存中;而鎖的釋放隱含着沖刷處理器緩存這個動作,這使得寫線程對共享變量所做的更新能夠被推送到該線程執行處理器的高速緩存中,從而對讀線程可同步。
  • 鎖能夠保障有序性,寫線程在臨界區中所執行的一系列操作在讀線程所執行的臨界區看起來像是完全按照源代碼順序執行的,即讀線程對這些操作的感知順序與源代碼順序一致。

可重入性:一個線程在其持有一個鎖的時候能否再次申請該鎖。

可重入鎖可以被理解爲一個對象,該對象包含一個計數器屬性。計數器屬性的初始值爲0,表示相應的鎖還沒有被任何線程持有。每次線程獲得一個可重入鎖的時候,該鎖的計數器值會增加1.每次一個線程釋放鎖的時候,該鎖的計數器屬性就會被減1.一個可重入鎖的持有線程初次獲得該鎖時相應的開銷相對大,這是因爲該鎖的持有線程必須與其他線程競爭以獲得鎖。可重入鎖的持有線程繼續獲得相應鎖所產生的開銷小得多,這是因爲此時java虛擬機只需要將相應鎖的計數器屬性增加1即可。

 java平臺中鎖的調度包括公平策略和非公平策略,鎖也分爲公平鎖和非公平鎖。內部鎖屬於非公平鎖,顯式鎖既支持公平鎖又支持非公平鎖。

3.3 內部鎖:synchronized關鍵字

java平臺中的任何一個對象都有一個唯一一個與之關聯的鎖。這種鎖被稱爲監視器或者內部鎖。內部鎖是一種排他鎖,它能夠保障原子性、可見性和有序性。

內部鎖的鎖句柄變量一般使用final修飾。這是因爲鎖句柄變量的值一旦改變,會導致執行同一個同步塊的多個線程實際上使用不同的鎖,從而導致竟態。同步竟態方法相當於以當前類對象爲引導鎖的同步塊。

package JavaCoreThreadPatten.capter03;

public class LockTest {
    /**
     * 默認以當前類實例作爲鎖對象
     */
    public synchronized void innerLock1(){
        
    }
    private final Object lock = new Object();

    /**
     * 指定鎖對象
     */
    public void innerLock2(){
        synchronized (lock){
            
        }
    }

    /**
     * 默認以當前類的class類實例作爲鎖對象
     */
    public synchronized static void innerLock3(){
        
    }
}

3.4 顯式鎖:Lock接口

ReentranLock是Lock接口的默認實現。一般鎖的釋放放在finally語句塊中執行,防止出現異常導出鎖泄漏。

ReentranLock即支持公平鎖也支持非公平鎖,通過構造參數中的fair參數來指定創建的是公平鎖還是非公平鎖。

公平鎖保障鎖調度的公平性往往是以增加了線程的暫停和喚醒的可能性,即增加了上下文切換爲代價的。因此,公平鎖適合於鎖被持有的時間相對長或者線程申請鎖的平均間隔時間相對長的情況。總的來說使用公平鎖的開銷比使用非公平鎖的開銷要大,因此顯示鎖默認使用的是非公平調度策略。

顯式鎖的使用更靈活,可以使用tryLock()來提升我們系統的性能,但是稍不注意會出現鎖泄漏;內部鎖不需要考慮鎖泄漏問題,jvm會幫我們進行釋放。

從《深入理解java虛擬機》這種書中其實推薦我們使用內部鎖,因爲內部鎖在1.6之後做了一些優化,並且內部鎖是隱性鎖,jvm優化方便。

讀寫鎖

讀寫鎖是一種改進型的排它鎖,也被稱爲共享/排他鎖。讀寫鎖允許多個線程可以同時讀取共享變量,但是一次只允許一個線程對共享變量進行更新。任何線程讀取共享變量的時候,其他線程無法更新這些變量;一個線程更新共享變量的時候,其他任何線程都無法訪問該變量。

讀鎖是共享的、寫鎖是排他的。讀鎖對讀線程來說起到保護其訪問的共享變量在其訪問期間不被修改的作用,並並使多個讀線程可以同時讀取這些變量從而提高併發性;而寫鎖保障了寫線程能夠以獨佔的方式安全的更新共享變量。ReadWriteLock接口及其實現類ReentrantReadWriteLock

package JavaCoreThreadPatten.capter03;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 讀寫鎖降級,先獲取寫鎖,然後獲取讀鎖
 */
public class ReadWriteLockDowngrade {
    private final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
    private final Lock READ_LOCK = READ_WRITE_LOCK.readLock();
    private final Lock WRITE_LOCK = READ_WRITE_LOCK.writeLock();

    public void operationWithLockDowngrade(){
        boolean readLockAcquired = false;
        WRITE_LOCK.lock();
        try{
            //執行寫業務邏輯操作
            System.out.println("執行寫操作");
            READ_LOCK.lock();
            readLockAcquired = true;
            //鎖降級成功,即獲取到讀鎖
            System.out.println("鎖降級成功,即獲取到讀鎖");
        }finally {
            WRITE_LOCK.unlock();
        }
        if(readLockAcquired){
            try{
                //執行讀操作
                System.out.println("執行讀操作");
            }finally {
                READ_LOCK.unlock();
            }
        }
    }

    public static void main(String[] args){
        ReadWriteLockDowngrade readWriteLockDowngrade = new ReadWriteLockDowngrade();
        readWriteLockDowngrade.operationWithLockDowngrade();
    }
}

由於讀寫鎖內部實現比內部鎖和其他顯式鎖要複雜的多,因此讀寫鎖適合於下面場景使用:1.只讀操作比寫操作要頻繁得多;2.讀線程持有鎖的時間比較長。只有同時滿足上面兩個條件的時候,讀寫鎖纔是適宜的選擇。ReentrantReadWriteLock所實現的讀寫鎖是個可重入鎖。ReentrantReadWriteLock支持鎖的降級,即一個線程持有讀寫鎖的寫鎖的情況下可以繼續獲得相應的讀鎖。

3.5 鎖的試用場景

1.check-then-act操作

2.read-modify-write操作

3.多個線程對多個共享數據進行更新

3.6 線程同步機制的底層助手:內存屏障

線程獲得和釋放鎖時所分別執行的兩個動作:刷新處理器緩存和沖刷處理器緩存。對於同一個鎖所保護的共享數據而言,前一個動作保證了該鎖的當前持有線程能夠讀取到前一個持有線程對這些數據所做的更新,後一個動作保證了該鎖的持有線程對這些數據所做的更新對該鎖的後序持有線程可見。

java虛擬機底層實際上是藉助內存屏障來實現刷新處理器緩存和沖刷處理器緩存兩個動作的。內存屏障是對一類僅針對內存讀、寫操作指令的跨處理器架構的比較底層的抽象。內存屏障是被插入到兩個指令之間進行使用的,其作用是禁止編譯器、處理器重排序從而保障有序性。它在指令序列中就像是一堵牆一樣使其兩側的指令無法穿越它。但是,爲了實現禁止重排序的功能,這些指令也往往具有一個副作用--刷新處理器緩存、沖刷處理器緩存,從而保證可見性。

內存屏障可以劃分爲以下幾類:

  • 按照可見性保障來劃分,內存屏障可以分爲加載屏障和存儲屏障。加載屏障的作用是刷新處理器緩存,存儲屏障的作用沖刷處理器緩存。java虛擬機會在MonitorExit(釋放鎖)對應的機器碼指令之後插入一個存儲屏障,這就保障了寫線程在釋放鎖之前在臨界區中對共享變量所做的更新對讀線程的執行處理器來說是可同步的;響應的,java虛擬機會在MonitorEnter(申請鎖)對應的機器碼指令之後臨界區開始之前的一個地方插入一個加載屏障,這使得讀線程的執行處理器能夠將寫線程對相應共享變量所做的更新從其他處理器同步到該處理器的告訴緩存中。因此,可見性的保障是通過寫線程和讀線程成對的使用存儲品章和加載屏障實現的。
  • 按照有序性保障劃分,內存屏障可以分爲獲取屏障和釋放屏障。獲取屏障的使用方式是在一個讀操作之後插入該內存屏障,其作用是禁止該讀操作與其後的任何讀寫操作之間進行重排序,這相當於在進行後續操作之前先要獲得相應共享數據的所有權。釋放屏障的使用方式是在一個寫操作之前插入該內存屏障,其作用是禁止該寫操作與其前面的任何讀寫操作之間進行重排序。這相當於在對相應共享數據操作結束後的釋放所有權。java虛擬機會在MonitorEnter對應的機器碼指令之後臨界區之前開始的地方插入一個獲取屏障,並在臨界區結束之後MonitorExit對應的機器碼指令之前的地方插入一個釋放屏障。
  • 由於獲取屏障禁止了臨界區中的任何讀、寫操作被重排序到臨界區之前的可能性,而釋放屏障又禁止了臨界區中的任何讀、寫操作被重排序到臨界區之後的可能性,因此臨界區內的任何讀、寫操作都無法被重排序到臨界區之外。在鎖的排他性的作用下,這使得臨界區中執行的操作序列具有原子性。因此,寫線程在臨界區中對各個共享變量所做的更新會同時對讀線程可見,即在讀線程看來各個共享變量就像是一下子被更新的,於是這些線程無從區分這些共享變量是以何種順序被更新的。這使得寫線程在臨界區中執行的操作自然而然的具有有序性。可見,鎖對有序性的保障是通過寫線程和讀線程配對使用釋放屏障與加載屏障實現的。

3.7 鎖與重排序

重排序規則:

1.臨界區內的操作不允許被重排序到臨界區之外

2.臨界區內的操作之間允許重排序

3.臨界區外的操作之間可以重排序

4.鎖申請與鎖釋放操作不能被重排序

5.兩個鎖申請操作不能被重排序

6.兩個鎖釋放操作不能被重排序

7.臨界區外的操作可以被重排序到臨界區之內

3.8 輕量級同步機制:volatile關鍵字

volatile修飾的變量讀和寫操作都必須從高速緩存或者主內存中讀取,以讀取變量的相對新值。因此,volatile變量不會被編譯器分配到寄存器進行存儲,對volatile變量的讀寫操作都是內存訪問操作。

volatile關鍵字常被稱爲輕量級鎖,保證可見性和有序性。在原子性方面它僅能保障寫volatile變量操作的原子性,但沒有鎖的排他性;其次,volatile關鍵字的使用不會引起上下文切換。

volatile關鍵字的作用包括:保障可見性、保障有序性和保障long/double型變量讀寫操作的原子性。volatile關鍵字在原子性方面僅保障對被修飾的變量的讀操作、寫操作本身的原子性。如果要保障對volatile變量的賦值操作的原子性,那麼這個賦值操作不能涉及任何共享變量的訪問。

對於volatile變量的寫操作,java虛擬機會在該操作之前插入一個釋放屏障,並在該操作之後插入一個存儲屏障:

其中,釋放屏障禁止了volatile寫操作與該操作之前的任何讀、寫操作進行重排序,從而保證了volatile寫操作之前的任何讀、寫操作會先於volatile寫操作被提交,即其他線程看到寫線程對volatile變量的更新時,寫線程在更新volatile變量之前所執行的內存操作的結果對於讀線程必然也是可見的。這就保障了讀線程對寫線程在更新volatile變量前對共享變量所執行的更新操作的感知順序與相應的源代碼順序一致,即保障了有序性。

對於volatile變量讀操作,java虛擬機會在該操作之前插入一個加載屏障,並在該操作之後插入一個獲取屏障,並在該操作之後插入一個獲取屏障:

其中,加載屏障通過沖刷處理器緩存,使其執行線程所在的處理器將其他處理器對共享變量所做的更新同步到該處理器的高速緩存中。讀線程執行的加載屏障和寫線程執行的存儲屏障配合在一起使得寫線程對volatile變量的寫操作以及在此之前所執行的其他內存操作的結果對讀線程可見,即保障了可見性。因此,volatile不僅僅保障了volatile變量本身的可見性,還保障了寫線程在更新volatile變量之前執行的所有操作的結果對讀線程可見。這種可見性保障類似於鎖對可見性的保障,與鎖不同的是volatile不具備排他性(此處我的理解其實volatile的寫和讀操作具備排他性,只不過僅限對與該變量的讀和寫操作具備排他性,即當某線程讀和寫該變量時是排他的,不知道我理解的對不對?),因而他不能保障讀線程讀取到的這些共享變量的值是最新的,即讀線程讀取到這些共享變量的那一刻可能已經有其他寫線程更新了這些共享變量的值。另外,獲取屏障禁止了volatile讀操作之後的任何讀、寫操作與volatile讀操作進行重排序。因此它保障了volatile讀操作之後的任何操作開始執行之前,寫線程對相關共享變量的更新已經對當前線程可見。

  • 寫volatile變量操作與該操作之前的任何讀、寫操作不會被重排序;
  • 讀volatile變量操作與該操作之後的任何讀、寫操作不會被重排序;

volatile變量在可見性方面僅僅是保證讀線程能夠讀取到共享變量的相對新值。對於引用型變量和數組變量,volatile關鍵字不能保證讀線程能夠讀取到相應對象字段、元素的相對新值

volatile變量的開銷包括讀變量和寫變量兩個方面。volatile變量的讀、寫操作都不會導致上下文切換,因此volatile的開銷比鎖要小。 

單例模式

package JavaCoreThreadPatten.capter03.singleton;

/**
 * 單例模式,含多線程安全問題
 * 懶漢模式
 */
public class SingleThreadedSingletonV1 {
    public static SingleThreadedSingletonV1 singleThreadedSingletonV1 = null;
    private SingleThreadedSingletonV1(){}

    /**
     * 方法體中實際上是check-then-act模式操作,存在線程安全問題
     * @return
     */
    public static SingleThreadedSingletonV1 getInstance(){
        if(singleThreadedSingletonV1 == null){
            singleThreadedSingletonV1 = new SingleThreadedSingletonV1();
        }
        return singleThreadedSingletonV1;
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 可用版本
 * 單例:餓漢模式
 * 不存在線程安全問題,但是程序啓動即創建實例,可能存在用不到的情況,浪費空間
 */
public class SingleThreadSingletonV2 {
    private static SingleThreadSingletonV2 singleThreadSingletonV2 = new SingleThreadSingletonV2();

    private SingleThreadSingletonV2(){}

    public static SingleThreadSingletonV2 getInstance(){
        return singleThreadSingletonV2;
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 通過內部鎖,給類加鎖保證getInstance方法是串行,不會出現併發問題,但是這種方式比較影響性能,因爲經過第一次創建完成實例後,獲取實例仍然需要串行化,併發量高時,壓力比較大
 */
public class SingleThreadSingletonV3 {
    private static SingleThreadSingletonV3 singleThreadSingletonV3 = null;
    private SingleThreadSingletonV3(){}

    public static SingleThreadSingletonV3 getInstance(){
        synchronized (SingleThreadSingletonV3.class){
            if(null==singleThreadSingletonV3){
                singleThreadSingletonV3 = new SingleThreadSingletonV3();
            }
            return singleThreadSingletonV3;
        }
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 通過雙重檢查鎖的方式既保證了併發情況下不會創建多次,同時也考慮的併發情況下性能問題
 * 但是仍然存在問題,解釋如下:儘管第一次檢查對變量instance的訪問沒有加鎖從而使竟態仍然可以存在,
 * 但是乍一看,它似乎既避免了鎖的開銷又保障了線程安全:一個線程T1執行到操作1的時候發現instance爲        null,而此刻另外一個線程T2可能恰好剛執行完操作3而使instance值不是null;接着T1獲得鎖而執行臨界區內的時候會再次判斷instance值是否爲null,此時由於該線程是在臨界區內讀取共享變量instance的,因此T1可以發現此刻instance值已經不爲null,於是,T1不會操作3,從而避免了再次創建一個實例。當然,僅僅從可見性的角度分析結論確實如此。但是,在一些情形下爲了確保線程安全光考慮可見性是不夠的,我們還需要考慮重排序的因素。操作3可以分解爲以下僞代碼:
objRef = allocate(SingleThreadSingletonV4 .class)//子操作1:分配對象所需的存儲空間
invokeConstructor(objRef);//子操作2:初始化objRef引用的對象
instance = objeRef;//子操作3:將對象引用寫入共享變量
    根據鎖的重排序規則,臨界區內的操作可以在臨界區內被重排序。因此,JIT編譯器可能將上述的子操作重排序爲:子操作1->子操作3->子操作2,即在初始化對象之前將對象的引用寫入實例變量instance。由於鎖對有序性的保障是有條件的,而操作1讀取instance變量的時候並沒有加鎖,因此上述重排序對操作1的執行線程是有影響的:該線程可能看到一個未初始化的實例,即變量instance的值不爲null,但是該變量所引用的對象中的某些實例變量的變量值可能仍然是默認值,而不是構造器中設置的初始值。也就是說,一個線程在執行操作1的時候發現instance不爲null,於是該線程就直接返回這個instance變量所引用的實例,而這個實例可能是未初始化完畢的,這就可能導致程序出錯。---------個人沒看明白,鎖不應該是保證了原子性、一致性、有序性,我認爲應該即使在臨界區內發生了重排序,但是對臨界區外都是透明的,這裏爲什麼在臨界區內發生重排序會對操作1有影響??
 */
public class SingleThreadSingletonV4 {
    private static SingleThreadSingletonV4 instance= null;
    private SingleThreadSingletonV4 (){}

    public static SingleThreadSingletonV4 getInstance(){
        if(null==instance){//操作1:第一次檢查
            synchronized (SingleThreadSingletonV4.class){
                if(null==instance){//操作2:第二次檢查
                    instance= new SingleThreadSingletonV4();//操作3
                }
            }
        }
        return instance;
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 可用版本:
 * 單例模式最終版本:雙重檢查鎖+volatile
 */
public class SingleThreadSingletonFinalV1 {
    private static volatile SingleThreadSingletonFinalV1 singleThreadSingletonFinal = null;
    private SingleThreadSingletonFinalV1(){}

    public static SingleThreadSingletonFinalV1 getInstance(){
        if(null==singleThreadSingletonFinal){
            synchronized (SingleThreadSingletonFinalV1.class){
                if(null==singleThreadSingletonFinal){
                    singleThreadSingletonFinal = new SingleThreadSingletonFinalV1();
                }
            }
        }
        return singleThreadSingletonFinal;
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 可用版本:
 * 通過內部類的方式,同樣也是延遲初始化
 *
 */
public class SingleThreadSingletonFinalV2 {
    private SingleThreadSingletonFinalV2 (){}

    private static class INSTANCE_HOLDER{
        final static SingleThreadSingletonFinalV2 SINGLE_THREAD_SINGLETON_FINAL_V_2 = new SingleThreadSingletonFinalV2();
    }

    public static SingleThreadSingletonFinalV2 getInstance(){
        return INSTANCE_HOLDER.SINGLE_THREAD_SINGLETON_FINAL_V_2;
    }

    public void someService(){
        //一些處理方法
    }
}
package JavaCoreThreadPatten.capter03.singleton;

/**
 * 可用版本
 * 枚舉
 */
public enum  SingleThreadSingletonFinalV3 {
    INSTANCE;

    public void someService(){
        //一些處理方法
    }
}

3.9 CAS

cas能夠將read-modify-write和check-and-act之類的操作轉換爲原子操作。

cas是一個原子的if-then-act的操作,其背後的假設是:當一個客戶(線程)執行cas操作的時候,如果變量V的當前值和客戶請求(即調用)CAS時所提供的變量值A(即變量的舊值)是相等的,那麼就說明其他線程並沒有修改過變量V的值。執行cas時如果沒有其他線程修改過變量V的值,那麼下手最快的客戶就會搶先將V的值更新爲B,而其他客戶的更新請求就會失敗。【個人理解有點樂觀鎖的感覺】

CAS僅保障共享變量更新操作的原子性,它並不保障可見性。

原子操作工具:java.util.concurrent.atomic包下面的類:

ABA問題:共享變量經歷了ABA的更新,那麼這種如果不能接受,那麼可以給變量增加版本號來解決。

3.10 對象的發佈和逸出

線程安全問題產生的前提條件是多個線程共享變量。

對象的發佈是指使對象能夠被其作用域之外的線程訪問:1.將對象的引用存儲到public變量中;2.在非private方法中返回一個對象;3.創建內部類,使得當前對象能夠被這個內部類使用。4.通過方法調用將對象傳遞給外部方法。

靜態變量的作用:一個類被java虛擬機加載之後,該類的所有靜態變量值仍然是默認值,直到有個線程初次訪問了該類的任意一個靜態變量才使這個類被初始化,類的所有靜態變量被賦予初始值。

static關鍵字僅僅保障讀線程能夠讀取到相應字段的初始值,而不是相對新值。

final關鍵字只能保證有序性

當一個對象的引用對其他對象可見的時候,這些線程所看到的該對象的final字段必然是初始化完畢的。final關鍵字的作用僅是這種有序性的保障,它並不能保障包含final字段的對象的引用自身對其他線程的可見性。

發佈了35 篇原創文章 · 獲贊 3 · 訪問量 5965
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章