來源:blog.csdn.net/randompeople/article/details/114917087 為什麼 java wait/notify 必須與 synchronized 一起使用 這個問題就是書本上沒怎麼講解,就是告訴我們這樣處理,但沒有解釋為什麼這麼處理?我也是基於這樣的困惑去了 ...
來源:blog.csdn.net/randompeople/article/details/114917087
為什麼 java wait/notify 必須與 synchronized 一起使用
這個問題就是書本上沒怎麼講解,就是告訴我們這樣處理,但沒有解釋為什麼這麼處理?我也是基於這樣的困惑去瞭解原因。
synchronized是什麼
Java中提供了兩種實現同步的基礎語義:synchronized方法和synchronized塊, 看個demo:
public class SyncTest {
\\ 1、synchronized方法
public synchronized void syncMethod(){
System.out.println("hello method");
}
\\ 2、synchronized塊
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
}
具體還要區分:
- 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。不同實例對象的訪問,是不會形成鎖的。
- 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
- 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
它具有的特性:
- 原子性
- 可見性
- 有序性
- 可重入性
synchronized如何實現鎖
這樣看來synchronized實現的鎖是基於class對象來實現的,我們來看看如何實現的,它其實是跟class對象的對象頭一起起作用的,對象在記憶體中的佈局分為三塊區域:對象頭、實例數據和對齊填充。
其中對象頭中有一個Mark Word,這裡主要存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息,把可能的情況列出來大概如下:
其中synchronized就與鎖標誌位一起作用實現鎖。主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(也稱為管程或監視器鎖)的起始地址。
每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。
在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的):
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
上面有2個欄位很重要:
_WaitSet隊列
處於wait狀態的線程,會被加入到_WaitSet。_EntryList隊列
處於等待鎖block狀態的線程,會被加入到該列表。_owner
_owner指向持有ObjectMonitor對象的線程
我們來模擬一下進入鎖的流程:
1、當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList
集合
2、當線程獲取到對象的monitor 後進入 _Owner
區域,並把monitor中的owner變數設置為當前線程同時monitor中的計數器count加1
3、若線程調用 wait()
方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。
4、若當前線程執行完畢也將釋放monitor(鎖)
並複位變數的值,以便其他線程進入獲取monitor(鎖)
wait/notify
這兩個是Java對象都有的屬性,表示這個對象的一個等待和通知機制。
推薦一個開源免費的 Spring Boot 最全教程:
不用synchronized 會怎麼樣
參考其他博客,我們來看看不使用synchronized會怎麼樣,假設有2個線程,分別做2件事情,T1線程代碼邏輯:
while(!條件滿足) // line 1
{
obj.wait(); // line 2
}
doSomething();
T2線程的代碼邏輯:
更改條件為滿足; // line 1
obj.notify(); // line 2
多線程環境下沒有synchronized,沒有鎖的情況下可能會出現如下執行順序情況:
- T1 line1 滿足while 條件
- T2 line1 執行
- T2 line2 執行,notify發出去了
- T1 line2 執行,wait再執行
這樣的執行順序導致了notify通知發出去了,但沒有用,已經wait是在之後執行,所以有人說沒有保證原子性,就是line1 和line2 是一起執行結束,這個也被稱作lost wake up
問題。解決方法就是可以利用synchronized來加鎖,於是有人就寫了這樣的代碼:
synchronized(lock)
{
while(!條件滿足)
{
obj.wait();
}
doSomething();
}
synchronized(lock)
{
更改條件為滿足;
obj.notify();
}
這樣靠鎖來做達到目的。但這代碼會造成死鎖,因為先T1 wait()
,再T2 notify();
而問題在於T1持有lock後block住了,T2一直無法獲得lock,從而永無可能notify()
並將T1的block狀態解除,就與T1形成了死鎖。
所以JVM在實現wait()
方法時,一定需要先隱式的釋放lock,再block,並且被notify()
後從wait()
方法返回前,隱式的重新獲得了lock後才能繼續user code的執行。要做到這點,就需要提供lock引用給obj.wait()
方法,否則obj.wait()
不知道該隱形釋放哪個lock,於是調整之後的結果如下:
synchronized(lock)
{
while(!條件滿足)
{
obj.wait(lock);
// obj.wait(lock)偽實現
// [1] unlock(lock)
// [2] block住自己,等待notify()
// [3] 已被notify(),重新lock(lock)
// [4] obj.wait(lock)方法成功返回
}
doSomething();
}
[最終形態] 把lock和obj合一
其它線程API如PThread提供wait()
函數的簽名是類似cond_wait(obj, lock)
的,因為同一個lock可以管多個obj條件隊列。而Java內置的鎖與條件隊列的關係是1:1,所以就直接把obj當成lock來用了。因此此處就不需要額外提供lock,而直接使用obj即可,代碼也更簡潔:
synchronized(obj)
{
while(!條件滿足)
{
obj.wait();
}
doSomething();
}
synchronized(lock)
{
更改條件為滿足;
obj.notify();
}
lost wake up
wait/notify 如果不跟synchronized結合就會造成lost wake up,難以喚醒wait的線程,所以單獨使用會有問題。
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!