java併發之synchronized

onlythinking發表於2020-06-19

Java為我們提供了隱式(synchronized宣告方式)和顯式(java.util.concurrentAPI程式設計方式)兩種工具來避免執行緒爭用。

本章節探索Java關鍵字synchronized。主要包含以下幾個內容。

  • synchronized關鍵字的使用;
  • synchronized背後的Monitor(管程);
  • synchronized保證可見性和防重排序;
  • 使用synchronized注意巢狀鎖定。

使用方式

synchronized 關鍵字有以下四種使用方式。

  1. 例項方法
  2. 靜態方法
  3. 例項方法中的程式碼塊
  4. 靜態方法中的程式碼塊
// 例項方法同步和例項方法程式碼塊同步
public class SynchronizedTest {
    private int count;
    public void setCountPart(int num) {
        synchronized (this) {
            this.count += num;
        }
    }
    public synchronized void setCount(int num) {
        this.count += num;
    }
}
// 靜態方法同步和靜態方法程式碼塊同步
public class SynchronizedTest {
    private static int count;
    public static void setCountPart(int num) {
        synchronized (SynchronizedTest.class) {
            count += num;
        }
    }
    public static synchronized void setCount(int num) {
        count += num;
    }
}

使用關鍵字synchronized實現同步是在JVM內部實現處理,對於應用開發人員來說它是隱式進行的。

每個Java物件都有一個與之關聯的monitor。

當執行緒呼叫例項同步方法時,會自動獲取例項物件的monitor。

當執行緒呼叫靜態同步方法時,會自動獲取該類Class例項物件的monitor。

Class例項:JVM為每個載入的class建立了對應的Class例項來儲存class及interface的所有資訊;

Monitor(管程)

Monitor 直譯為監視器,中文圈裡稱為管程。它的作用是讓執行緒互斥,保護共享資料,另外也可以向其它執行緒傳送滿足條件的訊號

如下圖,執行緒通過入口佇列(Entry Queue)到達訪問共享資料,若有執行緒佔用轉移等待佇列(Wait Queue),執行緒訪問共享資料完後觸發通知或轉移到訊號佇列(Signal Queue)。

Monitor

關於管程模型

網上查詢很多文章,大多數羅列 “ Hasen 模型、Hoare 模型和 MESA模型 ”這些名詞,看過之後我還是一知半解。本著對知識的求真,查詢溯源,找到了以下資料。

為什麼會有這三種模型?

假設有兩個執行緒A和B,執行緒B先進入monitor執行,執行緒A處於等待。當執行緒A執行完準備退出的時候,是先退出monitor還是先喚醒執行緒A?這時就出現了Mesa語義, Hoare語義和Brinch Hansen語義 三種不同版本的處理方式。

Mesa Semantics

Mesa模型中 執行緒只會出現在WaitQueue,EntryQueue,Monitor。

當執行緒B發出訊號告知執行緒A時,執行緒A從WaitQueue 轉移到EntryQueue並等待執行緒B退出Monitor之後再進入Monitor。也就是先通知再退出。

Monitor Mesa

Brinch Hanson Semantics

Brinch Hanson模型和Mesa模型類似區別在於僅允許執行緒B退出Monitor後才能傳送訊號給執行緒A。也就是先退出再通知。

Brinch Hanson

Hoare Semantics

Hoare模型中 執行緒會分別出現在WaitQueue,EntryQueue,SignalQueue,Monitor中。

當執行緒B發出訊號告知執行緒A並且退出Monitor轉移到SignalQueue,執行緒A進入Monitor。當執行緒A離開Monitor後,執行緒B再次回到Monitor。

Monitor Hoare

https://www.andrew.cmu.edu/course/15-440-kesden/applications/ln/lecture6.html

https://cseweb.ucsd.edu/classes/sp17/cse120-a/applications/ln/lecture8.html

Java裡面monitor是如何處理?

我們通過反編譯class檔案看下Synchronized工作原理。

public class SynchronizedTest {
    private int count;
    public void setCountPart(int num) {
        synchronized (this) {
            this.count += num;
        }
    }
}

編譯和反編譯命令

javac SynchronizedTest.java
javap -v SynchronizedTest

我們看到兩個關鍵指令 monitorentermonitorexit

Synchronized

monitorenter

Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread ......

每個物件都有一個關聯monitor。

執行緒執行 monitorenter 時嘗試獲取關聯物件的monitor。

獲取時如果物件的monitor被另一個執行緒佔有,則等待對方釋放monitor後再次嘗試獲取。

如果獲取成功則monitor計數器設定為1並將當前執行緒設為monitor擁有者,如果執行緒再次進入計數器自增,以表示進入次數。

monitorexit

The current thread should be the owner of the monitor associated with the instance referenced by objectref......

執行緒執行monitorexit 時,monitor計數器自減,當計數器變為0時釋放物件monitor。

原文:https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc9.html

可見性和重排序

在介紹Java併發之記憶體模型的時候,我們提到過執行緒訪問共享物件時會先拷貝副本到CPU快取,修改後返回CPU快取,然後等待時機重新整理到主存。這樣一來另外執行緒讀到的資料副本就不是最新,導致了資料的不一致,一般也將這種問題稱為執行緒可見性問題

不過在使用synchronized關鍵字的時候,情況有所不同。執行緒在進入synchronized後會同步該執行緒可見的所有變數,退出synchronized後,會將所有修改的變數直接同步到主存,可視為跳過了CPU快取,這樣一來就避免了可見性問題。

另外Java編譯器和Java虛擬機器為了達到優化效能的目的會對程式碼中的指令進行重排序。但是重排序會導致多執行緒執行出現意想不到的錯誤。使用synchronized關鍵字可以消除對同步塊共享變數的重排序。

侷限與效能

synchronized給我們提供了同步處理的便利,但是它在某些場景下也存在侷限性,比如以下場景。

  • 讀多寫少場景。讀動作其實是安全,我們應該嚴格控制寫操作。替代方案使用讀寫鎖readwritelock。如果只有一個執行緒進行寫操作,可使用volatile關鍵字替代。
  • 允許多個執行緒同時進入場景。synchronized限制了每次只有一個執行緒可進入。替代方案使用訊號量semaphore。
  • 需要保證搶佔資源公平性。synchronized並不保證執行緒進入的公平性。替代方案公平鎖FairLock。

關於效能問題。進入和退出同步塊操作效能開銷很小,但是過大範圍設定同步或者在頻繁的迴圈中使用同步可能會導致效能問題。

可重入,在monitorenter指令解讀中,可以看出synchronized是可重入,重入一般發生在同步方法巢狀呼叫中。不過要防止巢狀monitor死鎖問題。

比如下面程式碼會直接造成死鎖。

    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    public void method1()   {
        synchronized (lock1) {
            synchronized (lock2) {
            }
        }
    }
    public void method2()   {
        synchronized (lock2) {
            synchronized (lock1) {
            }
        }
    }

現實情況中,開發一般都不會出現以上程式碼。但在使用 wait() notify() 很可能會出現阻塞鎖定。下面是一個模擬鎖的實現。

  1. 執行緒A呼叫lock(),進入鎖定程式碼執行。
  2. 執行緒B呼叫lock(),得到monitorObj的monitor後等待執行緒B喚醒。
  3. 執行緒A執行完鎖定程式碼後,呼叫unlock(),在嘗試獲取monitorObj的monitor時,發現有執行緒佔用,也一直掛起。
  4. 這樣執行緒A B 就互相干瞪眼!
public class Lock{
protected MonitorObj monitorObj = new MonitorObj();
    protected boolean isLocked = false;
    public void lock() throws InterruptedException{
        synchronized(this){
            while(isLocked){
                synchronized(this.monitorObj){
                    this.monitorObj.wait();
                }
            }
            isLocked = true;
        }
    }
    public void unlock(){
        synchronized(this){
            this.isLocked = false;
            synchronized(this.monitorObj){
                this.monitorObj.notify();
            }
        }
    }
}

總結

本文記錄Java併發程式設計中synchronized相關的知識點。

歡迎大家留言交流,一起學習分享!!!

相關文章