帶着問題閱讀
1、Synchronized如何使用,加鎖的粒度分別是什么
2、Synchronized的實現機制是什么
3、Synchronized是公平鎖嗎
4、Java對Synchronized做了哪些優化
Synchronized介紹
基本上所有的並發模式在解決線程沖突問題的時候,都是采用
序列化訪問共享資源
的方案。這意味着在給定時刻只允許一個任務訪問共享資源。通常這是通過在代碼前面加上一條鎖語句來實現的,這就使得在一段時間內只有一個任務可以運行這段代碼。因為鎖語句產生了一種互相排斥的效果,所以這種機制常常稱為互斥量(mutex)
。
為防止資源沖突,Java提供了Synchronized
用於解決互斥訪問的問題。當任務執行被Synchronized
修飾的代碼時,將先檢查鎖是否可用,然后獲取鎖、執行代碼,最后釋放鎖。考慮屋里有一個衛生間,多個人都需要單獨使用,為了使用衛生間,每個人都先敲門,看看能否使用,如果沒人使用他就進入衛生間並鎖上門,當其它人來的時候就會被擋在門外。
Synchronized使用方式
- 對象鎖
synchronized
可以用於修飾具體對象,如示例中分別對synObj
和this
對象加鎖,即synObj
和this
分別作為共享資源被用於互斥訪問,其中thread1
和thread2
同時訪問synObj
互斥,thread3
和thread4
同時訪問this(demo對象)
互斥。
public class Demo {
private Object synObj = new Object();
public void synObj() {
// 對synObj對象加鎖
synchronized(synObj) {
// 同步代碼
}
}
public void synThis() {
// 對當前對象加鎖
synchronized(this) {
// 同步代碼
}
}
}
Demo demo = new Demo();
// 假設以下四個線程同時運行
new Thread(demo::synObj).start(); // thread1
new Thread(demo::synObj).start(); // thread2
new Thread(demo::synThis).start(); // thread3
new Thread(demo::synThis).start(); // thread4
- 普通方法鎖
synchronized
也可用於修飾方法,修飾方法時鎖的對象即this
,因此如果類的多個方法上都添加了synchronized
,那么這幾個方法在同步執行時也是互斥的。
public class Demo {
public synchronized void test1() {};
public synchronized void test2() {};
}
Demo demo = new Demo();
// 以下兩個線程同步執行時是互斥的,都需要獲取demo對象的鎖
new Thread(demo::test1).start();
new Thread(demo::test2).start();
- 靜態方法鎖
以上兩種應用方式由於鎖的粒度都是對象,因此只能在並發調用同一個對象的方法是才會互斥,如果創建了Demo demo1 = new Demo()
和Demo demo2 = new Demo()
兩個對象並分別調用,就不會產生互斥。如要在多實例之間也達成互斥,則可以通過修飾靜態方法來達成。
public class StaticDemo {
private static Object obj = new Object();
public void test() {
synchronized(obj) {
// 同步代碼
}
}
public static synchronized void testStatic() {};
}
// 兩個線程互斥s
new Thread(StaticDemo::testStatic).start();
new Thread(StaticDemo::testStatic).start();
- 類鎖
通過添加類鎖,也可實現多實例之間的互斥。synchronized
修飾在靜態方法時,也等價於修飾當前類對象。
public class StaticDemo {
public void test() {
synchronized(StaticDemo.class) {
// 同步代碼
}
}
}
Synchronized原理分析
不論synchronized
用於修飾哪里,本質還是會修飾到具體的對象(實例對象或類對象)上,synchronized
的實現機制也是對對象的加鎖。Java中每個對象都隱含關聯一個監視器ObjectMonitor
,監視器通過cpp
實現內置在JVM中,監視器地址記錄在對象的MarkWord
上,synchronized
通過ObjectMonitor
實現對象的鎖操作。
對象頭MarkWord簡介
JVM在內存中將對象划為三部分:對象頭、實例數據和填充數據。對象頭分為MarkWord
和類型指針兩部分,這里只針對鎖相關做進一步介紹。MarkWord
用於存儲對象自身的運行數據,如哈希值、GC分代年齡等,這部分在32位和64位虛擬機中會分別占用32位和64位空間,以下是32位的空間布局(64位布局相同,分的bit數不同),MarkWord
會根據對象狀態復用存儲空間,例如對象未鎖定狀態下,采用25bit哈希 + 4bitGC年齡 + 1bit固定0 + 2bit標志存儲。當標志位為10表示對象處於重量級鎖定時,剩余空間就用於存儲ObjectMonitor
對象的地址。

ObjectMonitor簡介
ObjectMonitor() {
...
_count = 0; // 記錄個數
_owner = NULL; // 記錄持有線程
_cxq = NULL; // 記錄鎖阻塞線程,與EntryList配合
_WaitSet = NULL; // 記錄處於wait狀態的線程
_EntryList = NULL; // 記錄處於鎖阻塞狀態的線程
...
}
ObjectMonitor
整體內容略去,核心關注以上字段。_owner
用於記錄持有線程,_count
用於記錄重入次數,_cxq
和_EntryList
配合用於記錄獲取鎖失敗阻塞后的線程。
線程獲取鎖失敗后會首先被掛載到_cxq
隊列上並調用park
阻塞。當鎖被釋放時,如_EntryList
不為空,則嘗試喚醒_EntryList
隊首元素;如_EntryList
為空,默認從_cxq
摘取隊首元素放入_EntryList
並試圖獲取鎖。由於monitor
鎖機制為非公平鎖,因此可能喚醒失敗,兩個隊列都會保存阻塞元素。
詳細解析可見參考第二篇文章
Synchronized重量級鎖原理
public class Demo {
private Object obj = new Object();
public void test() {
synchronized(obj) {
System.out.println("lock");
}
}
}
編譯以上代碼,javap -v
查看字節碼。
...
public void test();
Code:
...
monitorenter // 加鎖
...
monitorexit // 釋放鎖
...
return
...
其余內容略去,關鍵在於monitorenter
和monitorexit
兩個指令。
當執行monitorenter
時,將會嘗試獲取該對象monitor
的所有權。
- 如果
monitor
持有數為0即無線程持有,則直接獲取monitor
並將進入數+1; - 如果
monitor
已被線程占有,檢查是否為當前線程,如是當前線程,則將計數器+1;否則阻塞當前線程。
當執行monitorexit
時,將monitor
計數器-1,如減后為0,則線程釋放monitor
。
如synchronized
修飾在方法上,則會在方法上增加ACC_SYNCHRONIZED
的標記,原理與上述相同。
JVM對Synchronized的優化
monitorenter
和monitorexit
依賴底層操作系統的mutex lock
實現,該指令對線程的掛起和喚醒涉及到用戶態到內核態的切換,如果同步代碼頻繁調用,會帶來昂貴的切換開銷。自jdk1.6起對鎖的實現引入了大量優化,下面來介紹一下都做了哪些優化。
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定一句來源於逃逸分析的數據支持,如果判斷一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上數據對待,認為是線程私有的,同步加鎖無須進行。
public String copyString(String s) {
StringBuffer sb = new StringBuffer();
sb.append(s);
return sb.toString();
}
如示例代碼,StringBuffer.append
是通過sychronized
修飾的線程安全操作,但在該代碼塊中,sb
對象是局部變量,僅會被當前線程訪問,不存在線程競爭,因此鎖經過編譯器檢測后可以消除。
鎖粗化
原則上編寫同步代碼時,推薦將同步塊的作用范圍限制的盡量小,一方面減少同步代碼塊的執行時間,一方面減少同步競爭次數,以便存在競爭時,等待鎖的線程可以盡快獲得鎖。但是如果一系列連續操作都在對同一對象反復加鎖和釋放鎖,那即使沒有線程競爭也會產生很多沒必要的開銷。
private Object obj = new Object();
public void lock() {
synchronized(obj) {
...
}
// 再次加鎖
synchronized(obj) {
...
}
}
如上代碼,連續兩次對同一對象進行同步,即可將鎖粗化合並為一個鎖。
鎖消除和鎖粗化都是依賴JIT即時編譯實現,因此通過
javac
查看編譯后的字節碼,仍然保留着原始的鎖指令。
自旋鎖和自適應自旋
前文中我們提到,互斥同步涉及的掛起/喚醒線程都涉及內核態轉換,如果頻繁產生競爭會帶來很大的壓力。虛擬機開發團隊注意到很多應用對鎖的持有只會持續很短的時間,如果可以讓競爭鎖的線程稍等一下,不放棄處理器,就可以在持有鎖的線程執行完畢后獲取鎖,避免產生空間切換,這就是自旋。
自旋鎖在jdk1.4.2中就引入,需要-XX: +UseSpining
開啟,在jdk6以后就默認開啟了。自旋雖然避免了空間切換問題,但如果某個鎖競爭很激烈或者鎖的持有時間很長,那自旋只能白白占用處理器資源,因此在jdk1.6中引入了自適應自旋。自適應意味着自旋的時間不再固定,如果對一個鎖對象自旋等待剛剛成功過,則允許后續自旋等待較長時間;如果自旋很少成功,那就在后續獲得鎖的過程中直接跳過自旋。
偏向鎖
偏向鎖也是jdk1.6引入的優化,目的是消除數據在無競爭情況下的同步原語。鎖被第一個線程獲取后,在接下來的執行過程中,如果一直沒有被其他線程獲取,則持有偏向鎖的線程不在需要同步。
偏向鎖加鎖流程如下:
- 檢查當前是否為偏向狀態。
- 如果是,檢查當前線程ID與
Mark Word
記錄的線程ID是否一致,如一致則進入同步代碼,不一致則釋放偏向鎖 - 如不是偏向鎖,則使用
CAS
嘗試修改線程ID,如修改成功則進入同步代碼,失敗則釋放偏向鎖
線程獲取偏向鎖后,持有鎖的線程以后每次進入相應同步塊時,都不需要再進行任何同步操作。
偏向鎖不會主動釋放,只有當其他線程嘗試獲取鎖時,才會檢查持有線程是否可以釋放鎖。如可以釋放則替換為新線程ID,不可釋放則升級為輕量級鎖。
勘誤:圖中如判斷對象頭Mark Word記錄非當前線程ID,下一步應當為開始偏向鎖撤銷而非CAS替換。如有不同意見歡迎留言
輕量級鎖
輕量級鎖在MarkWord
標志位中由00表示,輕量級鎖首先在當前線程棧幀當中建立一個鎖記錄Lock Record
,用於存儲MarkWord
的拷貝;然后虛擬機使用CAS
操作將Lock Record
的地址記錄到MarkWord
當中,並將標志位改為00,表示對象處於輕量級鎖定狀態。如果更新失敗,則會進入自旋並在自旋達到一定次數后升級為重量級鎖。自旋的同時如果有第三個線程嘗試獲取鎖,也會直接升級到重量級鎖。
同步代碼執行完畢后,輕量級鎖同樣使用CAS
操作將棧幀中的MarkWord
拷貝回到對象中,如果操作成功,則釋放鎖;如果替換失敗,則說明有其他線程在競爭鎖(意味着升級為重量級鎖),則當前膨脹為重量鎖轉換為重量鎖的釋放。
重量級鎖
重量級鎖即上文Synchronized重量級鎖原理
所述內容,綜上synchronized
的加鎖過程為偏向鎖 -> 輕量級鎖 -> 重量級鎖,這個過程也稱為鎖膨脹。
圖源自網絡
總結
最后總結對比一下幾種鎖實現。
鎖類型 | 運行空間 | 實現機制 | 適用范圍 |
---|---|---|---|
偏向鎖 | 用戶態 | 初次CAS加鎖,后續如無競爭可直接進入 | 單線程執行 |
輕量級鎖 | 用戶態 | CAS+自旋加鎖 | 鎖競爭不激烈 |
重量級鎖 | 內核態 | mutex 內核態操作 | 鎖激烈競爭 |