Java關鍵詞synchronized解讀

範德依彪發表於2022-12-29

1 引入Synchronized

  1. Synchronized是java虛擬機器為執行緒安全而引入的。
  2. 互斥同步是一種最常見的併發正確性的保障手段。同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一條執行緒使用。
  3. synchronized是最基本的互斥同步手段,它是一種塊結構的同步語法。
  4. synchronized修飾程式碼塊,無論該程式碼塊正常執行完成還是發生異常,都會釋放鎖

synchronized對執行緒訪問的影響:

  • 被synchronized修飾的同步塊在持有鎖的執行緒執行完畢並釋放鎖之前,會阻塞其他執行緒的進入。
  • 被synchronized修飾的同步塊對同一條執行緒是可重入

2 Synchronized的使用

可以作用在方法上或者方法裡的程式碼塊:

  1. 修飾方法,包括例項方法和靜態方法
  2. 修飾方法裡的程式碼塊,這時需要一個引用作為引數
  3. Synchronized作用地方不同,產生的鎖型別也不同,分為物件鎖和類鎖

2.1 物件鎖

Synchronized修飾例項方法或者程式碼塊(鎖物件不是*.class),此時生產物件鎖。多執行緒訪問該類的同一個物件的sychronized塊是同步的,訪問不同物件不受同步限制。

2.1.1 Synchronized修飾例項方法

public static void main(String[] args){
        TempTest tempTest = new TempTest();
        Thread t1 = new Thread(() -> {
            tempTest.doing(Thread.currentThread().getName());
        });
        Thread t2 = new Thread(() -> {
            tempTest.doing(Thread.currentThread().getName());
        });
        t1.start();
        t2.start();
    }

    //同一時刻只能被一個執行緒呼叫
    private synchronized void doing(String threadName){
        for(int i=0;i<3;i++){
            System.out.println("current thread is : "+threadName);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }

執行結果:

current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1

2.1.2 Synchronized修飾程式碼塊

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();
    @Override
    public void run() {
        // 同步程式碼塊形式:鎖為this,兩個執行緒使用的鎖是一樣的,執行緒1必須要等到執行緒0釋放了該鎖後,才能執行
        synchronized (this) {
            System.out.println("我是執行緒" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

執行結果:

我是執行緒Thread-0
Thread-0結束
我是執行緒Thread-1
Thread-1結束

2.2 類鎖

synchronize修飾靜態方法或指定鎖物件為Class,此時產生類鎖。多執行緒訪問該類的所有物件的sychronized塊是同步的

2.2.1 synchronize修飾靜態方法

    public static void main(String[] args){
        TempTest tempTest1 = new TempTest();
        TempTest tempTest2 = new TempTest();
        //雖然建立了兩個TempTest例項,但是依然是呼叫同一個doing方法(因為是個static);因此doing還是會依次執行
        Thread t1 = new Thread(() -> tempTest1.doing(Thread.currentThread().getName()));
        Thread t2 = new Thread(() -> tempTest2.doing(Thread.currentThread().getName()));
        t1.start();
        t2.start();
    }

    //修飾靜態方法,則是類鎖;
    private static synchronized void doing(String threadName){
        for(int i=0;i<3;i++){
            System.out.println("current thread is : "+threadName);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }

執行結果:有序輸出 【如果去掉static ,則執行緒會交替執行doing】

current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1

2.2.2 synchronize指定鎖物件為Class

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 所有執行緒需要的鎖都是同一把
        synchronized(SynchronizedObjectLock.class){
            System.out.println("我是執行緒" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

結果:

我是執行緒Thread-0
Thread-0結束
我是執行緒Thread-1
Thread-1結束

3 Synchronized原理分析

3.1 虛擬機器如何辨別和處理synchronized

  • 虛擬機器可以從常量池中的方法表結構中的ACC_ SYNCHRONIZED訪問標誌區分一個方法是否是同步方法。
  • 當呼叫方法時,呼叫指令將會檢查方法的ACC_ SYNCHRONIZED訪問標誌是否設定,如果設定了,執行執行緒將先持有同步鎖,然後執行方法,最後在方法完成時釋放同步鎖。
  • 在方法執行期間,執行執行緒持有了同步鎖,其他任何執行緒都無法再獲得同一個鎖。
  • 如果一個同步方法執行期間丟擲了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的鎖將在異常拋到同步方法之外時自動釋放。

3.2 虛擬機器對synchronized的編譯處理

以下程式碼:

public class Foo {
    void onlyMe(Foo f) {
        synchronized(f) {
            doSomething();
        }
    }
    private void doSomething(){ }
}

編譯後,這段程式碼生成的位元組碼序列如下:
Java關鍵詞synchronized解讀

  1. synchronized關鍵字經過Javac編譯之後,會在同步塊的前後生成monitorentermonitorexit兩個位元組碼指令。
  2. 指令含義:monitorenter:獲取物件的鎖monitorexit:釋放物件的鎖
  3. 執行monitorenter指令時,首先嚐試獲取物件的鎖。如果物件沒被鎖定,或者當前執行緒已經持有了物件的鎖,就把鎖的計數器的值增加1
  4. 執行monitorexit指令時,將鎖計數器的值減1,一旦計數器的值為零,鎖隨即就被釋放
  5. 如果獲取物件鎖失敗,那當前執行緒阻塞等待,直到鎖被釋放。
  6. 為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理程式,它的目的就是用來執行monitorexit指令。

3.3 虛擬機器執行加鎖和釋放鎖的過程

那麼重點來了到這裡,有幾個問題需明確:

  1. 什麼叫物件的鎖
  2. 如何確定鎖被執行緒持有
  3. 執行monitorenter後,物件發生什麼變化
  4. 鎖計數值儲存在哪裡,如何獲取到?

1. 什麼叫物件的鎖?
物件的記憶體結構參考:2 Java記憶體層面的物件認識

  1. 鎖,一種可以被讀寫的資源,物件的鎖是物件的一部分。
  2. 物件的結構中有部分稱為物件頭
  3. 物件頭中有2bit空間,用於儲存鎖標誌,透過該標誌位來標識物件是否被鎖定。

2. 如果確定鎖被執行緒持有?

  1. 程式碼即將進入同步塊的時,如果鎖標誌位為“01”(物件未被鎖定),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄的空間,儲存鎖物件Mark Word的複製。(執行緒開闢空間並儲存物件頭)
  2. 虛擬機器將使用CAS操作嘗試把物件的Mark Word更新成指向鎖記錄的指標(物件頭的mw儲存指向執行緒“鎖記錄”中的指標)
  3. 如果CAS操作成功,即代表該執行緒擁有了這個物件的鎖,並且將物件的鎖標誌位轉變為“00”
  4. 如果CAS操作失敗,那就意味著至少存在一條執行緒與當前執行緒競爭獲取該物件的鎖。虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是,說明當前執行緒已經擁有了這個物件的鎖,那直接進入同步塊繼續執行,否則就說明這個鎖物件已經被其他執行緒搶佔。
  5. 解鎖過程:CAS操作把執行緒中儲存的MW複製替換回物件頭中。假如能夠成功替換,那整個同步過程就順利完成了;如果替換失敗,則說明有其他執行緒嘗試過獲取該鎖,就要在釋放鎖的同時,喚醒被掛起的執行緒。

3 執行monitorenter後,物件發生什麼變化?

  1. 物件的鎖標誌位轉變為“00”
  2. 擁有物件鎖的執行緒開闢了新空間,儲存了物件的Mark Word資訊
  3. 物件的Mark Word儲存了執行緒的鎖記錄空間的地址複製

4 鎖計數值儲存在哪裡
我還沒搞懂。

monitorenter指令執行的過程
Java關鍵詞synchronized解讀

4 Synchronized與Lock

synchronized的缺陷

  1. 在多執行緒競爭鎖時,當一個執行緒獲取鎖時,它會阻塞所有正在競爭的執行緒,這樣對效能帶來了極大的影響。
  2. 掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,上下文切換需要消耗很大效能。
  3. 效率低:鎖的釋放情況少,只有程式碼執行完畢或者異常結束才會釋放鎖;試圖獲取鎖的時候不能設定超時,不能中斷一個正在使用鎖的執行緒,相對而言,Lock可以中斷和設定超時
  4. 不夠靈活:加鎖和釋放的時機單一,每個鎖僅有一個單一的條件(某個物件),相對而言,讀寫鎖更加靈活

5 使用Synchronized有哪些要注意的

  • 鎖物件不能為空,因為鎖的資訊都儲存在物件頭裡
  • 作用域不宜過大,影響程式執行的速度,控制範圍過大,編寫程式碼也容易出錯
  • 在能選擇的情況下,既不要用Lock也不要用synchronized關鍵字,用java.util.concurrent包中的各種各樣的類,如果有必要,使用synchronized關鍵,因為程式碼量少,避免出錯
  • synchronized實際上是非公平的,新來的執行緒有可能立即獲得執行,而在等待區中等候已久的執行緒可能再次等待,這樣有利於提高效能,但是也可能會導致飢餓現象。

相關文章