多執行緒之synchronized關鍵字
synchronized關鍵字
1、為什麼需要同步器
多執行緒程式設計中,有可能會出現多個執行緒同時訪問同一個共享、可變資源的情況,這個資源我們稱之其為臨界資源;這種資源可能是:
物件、變數、檔案等。
共享:資源可以由多個執行緒同時訪問
可變:資源可以在其生命週期內被修改
引出的問題:
由於執行緒執行的過程是不可控的,所以需要採用同步機制來協同對物件可變狀態的訪問!
2、如何解決執行緒併發安全問題?
實際上,所有的併發模式在解決執行緒安全問題時,採用的方案都是序列化訪問臨界資源。即在同一時刻,只能有一個執行緒訪問臨
界資源,也稱作同步互斥訪問。
Java 中,提供了兩種方式來實現同步互斥訪問:****synchronized
同步器的本質就是加鎖
加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個執行緒訪問臨界資源(同步互斥訪問)
不過有一點需要區別的是:當多個執行緒執行一個方法時,該方法內部的區域性變數並不是臨界資源,因為這些區域性變數是在每個執行緒的
私有棧中,因此不具有共享性,不會導致執行緒安全問題。
synchronized內建鎖是一種物件鎖(鎖的是物件而非引用),作用粒度是物件,可以用來實現對臨界資源的同步互斥訪問,是可
重入的。
加鎖的方式:
1、同步例項方法,鎖是當前例項物件
2、同步類方法,鎖是當前類物件
3、同步程式碼塊,鎖是括號裡面的物件
synchronized是基於JVM內建鎖實現,通過內部物件Monitor
塊同步,監視器鎖的實現依賴底層作業系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖效能較低。當然,JVM內建鎖在1.5
之後版本做了重大的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,,內建鎖的併發效能已經基本與 Lock持平。
synchronized關鍵字被編譯成位元組碼後會被翻譯成monitorenter 和 monitorexit 兩條指令分別在同步塊邏輯程式碼的起始位置
與結束位置。
每個同步物件都有一個自己的Monitor(監視器鎖),加鎖過程如下圖所示:
從這裡我們可以得到一個資訊,也就是說這些線層也在進行操作,此時在多核作業系統下,可能一直在CPU上來進行執行。
Monitor監視器鎖
任何一個物件都有一個Monitor與之關聯,當且一個Monitor被持有後,它將處於鎖定狀態。Synchronized在JVM裡的實現都是
基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的MonitorEnter和
MonitorExit指令來實現。
monitorenter:每個物件都是一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行
monitorenter指令時嘗試獲取monitor的所有權,過程如下:
a. 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor
的所有者;
b. 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1;
c. 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗
試獲取monitor的所有權;
monitorexit:執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。指令執行時,monitor的進入數減
1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去
獲取這個 monitor 的所有權。
monitorexit,指令出現了兩次,第1次為同步正常退出釋放鎖;第2次為發生非同步退出釋放鎖;
通過上面兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的物件來
完成,其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則
會丟擲java.lang.IllegalMonitorStateException的異常的原因。
看一個同步方法
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
從編譯的結果來看,方法的同步並沒有通過指令 monitorenter 和 monitorexit 來完成(理論上其實也可以通過這兩條指令來
實現),不過相對於普通方法,其常量池中多了 ACC_SYNCHRONIZED 標示符。JVM就是根據該標示符來實現方法的同步的:
當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取
monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個
monitor物件。
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。兩個指令的執行是JVM通
過呼叫作業系統的互斥原語mutex來實現,被阻塞的執行緒會被掛起、等待重新排程,會導致“使用者態和核心態”兩個態之間來回切
換,對效能有較大影響。
什麼是monitor?
可以把它理解為 一個同步工具,也可以描述為 一種同步機制,它通常被 描述為一個物件。與一切皆物件一樣,所有的Java物件
是天生的Monitor,每一個Java物件都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java物件自打孃胎裡出來就帶了一把
看不見的鎖,它叫做內部鎖或者Monitor鎖。也就是通常說Synchronized的物件鎖,MarkWord鎖標識位為10,其中指標指向的
是Monitor物件的起始地址。在Java虛擬機器(HotSpot)中,Monitor是由ObjectMonitor實現的,其主要資料結構如下(位於
HotSpot虛擬機器原始碼ObjectMonitor.hpp檔案,C++實現的):
1 ObjectMonitor() {
2 _header = NULL;
3 _count = 0; // 記錄個數
4 _waiters = 0,
5 _recursions = 0;
6 _object = NULL;
7 _owner = NULL;
8 _WaitSet = NULL; // 處於wait狀態的執行緒,會被加入到_WaitSet
9 _WaitSetLock = 0 ;
10 _Responsible = NULL ;
11 _succ = NULL ;
12 _cxq = NULL ;
13 FreeNext = NULL ;
14 _EntryList = NULL ; // 處於等待鎖block狀態的執行緒,會被加入到該列表
15 _SpinFreq = 0 ;
16 _SpinClock = 0 ;
17 OwnerIsThread = 0 ;
18 }
ObjectMonitor中有兩個佇列,_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成
ObjectWaiter物件 ),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步程式碼時:
- 首先會進入 _EntryList 集合,當執行緒獲取到物件的monitor後,進入 _Owner區域並把monitor中的owner變數設定為當
前執行緒,同時monitor中的計數器count加1;
- 若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSet
集合中等待被喚醒;
- 若當前執行緒執行完畢,也將釋放monitor(鎖)並復位count的值,以便其他執行緒進入獲取monitor(鎖);
同時,Monitor物件存在於每個Java物件的物件頭Mark Word中(儲存的指標的指向),Synchronized鎖便是通過這種方式
獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因,同時notify/notifyAll/wait等方法會使用到Monitor鎖物件,所以必須
在同步程式碼塊中使用。監視器Monitor有兩種同步方式:互斥與協作。多執行緒環境下執行緒之間如果需要共享資料,需要解決互斥訪問
資料的問題,監視器可以確保監視器上的資料在同一時刻只會有一個執行緒在訪問。
千里之行,始於足下。不積跬步,無以至千里