1. 程式人生 > >java併發之synchronized

java併發之synchronized

Java為我們提供了隱式(synchronized宣告方式)和顯式(java.util.concurrentAPI程式設計方式)兩種工具來避免執行緒爭用。 本章節探索Java關鍵字synchronized。主要包含以下幾個內容。 - synchronized關鍵字的使用; - synchronized背後的Monitor(管程); - synchronized保證可見性和防重排序; - 使用synchronized注意巢狀鎖定。 ## 使用方式 synchronized 關鍵字有以下四種使用方式。 1. 例項方法 2. 靜態方法 3. 例項方法中的程式碼塊 4. 靜態方法中的程式碼塊 ```java // 例項方法同步和例項方法程式碼塊同步 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; } } ``` ```Java // 靜態方法同步和靜態方法程式碼塊同步 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](https://upload-images.jianshu.io/upload_images/19724385-e5e564191b419fe7.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **關於管程模型** 網上查詢很多文章,大多數羅列 “ *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](https://upload-images.jianshu.io/upload_images/19724385-6847e11d4f7073ab.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #### #### **Brinch Hanson Semantics** Brinch Hanson模型和Mesa模型類似區別在於僅允許執行緒B退出Monitor後才能傳送訊號給執行緒A。也就是先退出再通知。 ![Brinch Hanson](https://upload-images.jianshu.io/upload_images/19724385-1143c3f418899ba0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #### **Hoare Semantics** Hoare模型中 執行緒會分別出現在WaitQueue,EntryQueue,SignalQueue,Monitor中。 當執行緒B發出訊號告知執行緒A並且退出Monitor轉移到SignalQueue,執行緒A進入Monitor。當執行緒A離開Monitor後,執行緒B再次回到Monitor。 ![Monitor Hoare](https://upload-images.jianshu.io/upload_images/19724385-9f71c82aef5ab77e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) > 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工作原理。 ```Java public class SynchronizedTest { private int count; public void setCountPart(int num) { synchronized (this) { this.count += num; } } } ``` 編譯和反編譯命令 ```bash javac SynchronizedTest.java javap -v SynchronizedTest ``` 我們看到兩個關鍵指令 **monitorenter** 和 **monitorexit** ![Synchronized](https://upload-images.jianshu.io/upload_images/19724385-feb913efc340dbd0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #### **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併發之記憶體模型](https://www.onlythinking.com/2020/06/08/java%E5%B9%B6%E5%8F%91%E4%B9%8B%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/)的時候,我們提到過執行緒訪問共享物件時會先拷貝副本到CPU快取,修改後返回CPU快取,然後等待時機重新整理到主存。這樣一來另外執行緒讀到的資料副本就不是最新,導致了資料的不一致,一般也將這種問題稱為**執行緒可見性問題**。 不過在使用synchronized關鍵字的時候,情況有所不同。執行緒在進入synchronized後會同步該執行緒可見的所有變數,退出synchronized後,會將所有修改的變數直接同步到主存,可視為跳過了CPU快取,這樣一來就避免了可見性問題。 另外Java編譯器和Java虛擬機器為了達到優化效能的目的會對程式碼中的指令進行重排序。但是重排序會導致多執行緒執行出現意想不到的錯誤。使用synchronized關鍵字可以消除對**同步塊共享變數**的重排序。 ## 侷限與效能 synchronized給我們提供了同步處理的便利,但是它在某些場景下也存在侷限性,比如以下場景。 - 讀多寫少場景。讀動作其實是安全,我們應該嚴格控制寫操作。替代方案使用讀寫鎖readwritelock。如果只有一個執行緒進行寫操作,可使用volatile關鍵字替代。 - 允許多個執行緒同時進入場景。synchronized限制了每次只有一個執行緒可進入。替代方案使用訊號量semaphore。 - 需要保證搶佔資源公平性。synchronized並不保證執行緒進入的公平性。替代方案公平鎖FairLock。 關於效能問題。進入和退出同步塊操作效能開銷很小,但是*過大範圍設定同步*或者*在頻繁的迴圈中使用同步*可能會導致效能問題。 可重入,在monitorenter指令解讀中,可以看出synchronized是可重入,重入一般發生在同步方法巢狀呼叫中。不過要防止巢狀monitor死鎖問題。 比如下面程式碼會直接造成死鎖。 ```java 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 就互相干瞪眼! ```java 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相關的知識點。 歡迎大家留言交流,一起學習分享