java併發之synchronized
阿新 • • 發佈:2020-06-19
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相關的知識點。
歡迎大家留言交流,一起學習分享