java併發之synchronized

Java爲咱們提供了隱式(synchronized聲明方式)和顯式(java.util.concurrentAPI編程方式)兩種工具來避免線程爭用。html

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

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

使用方式

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

  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。segmentfault

當線程調用實例同步方法時,會自動獲取實例對象的monitor。緩存

當線程調用靜態同步方法時,會自動獲取該類Class實例對象的monitor。安全

Class實例:JVM爲每一個加載的class建立了對應的Class實例來保存class及interface的全部信息;

Monitor(管程)

Monitor 直譯爲監視器,中文圈裏稱爲管程。它的做用是讓線程互斥,保護共享數據,另外也能夠向其它線程發送知足條件的信號bash

以下圖,線程經過入口隊列(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/co...

https://cseweb.ucsd.edu/class...

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/javas...

可見性和重排序

在介紹Java併發以內存模型的時候,咱們提到過<u>線程訪問共享對象時會先拷貝副本到CPU緩存,修改後返回CPU緩存,而後等待時機刷新到主存。這樣一來另外線程讀到的數據副本就不是最新,致使了數據的不一致,通常也將這種問題稱爲線程可見性問題</u>。

不過在使用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相關的知識點。

歡迎你們留言交流,一塊兒學習分享!!!

相關文章
相關標籤/搜索