我在[上一講](http://time.geekbang.org/column/article/8799)對比和分析了 synchronized 和 ReentrantLock,算是專欄進入并發編程階段的熱身,相信你已經對線程安全,以及如何使用基本的同步機制有了基礎,今天我們將深入了解 synchronize 底層機制,分析其他鎖實現和應用場景。
今天我要問你的問題是 ,synchronized 底層如何實現?什么是鎖的升級、降級?
## 典型回答
在回答這個問題前,先簡單復習一下上一講的知識點。synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現[單元](https://docs.oracle.com/javase/specs/jls/se10/html/jls-8.html#d5e13622)。
在 Java 6 之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同的 Monitor 實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不同的競爭狀況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。
當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操作([compare and swap](https://en.wikipedia.org/wiki/Compare-and-swap)),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向于當前線程,所以并不涉及真正的互斥鎖。這樣做的假設是基于在很多應用場景中,大部分對象生命周期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競爭開銷。
如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖,并切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級為重量級鎖。
我注意到有的觀點認為 Java 不會進行鎖降級。實際上據我所知,鎖降級確實是會發生的,當 JVM 進入安全點([SafePoint](http://blog.ragozin.info/2012/10/safepoints-in-hotspot-jvm.html))的時候,會檢查是否有閑置的 Monitor,然后試圖進行降級。
## 考點分析
今天的問題主要是考察你對 Java 內置鎖實現的掌握,也是并發的經典題目。我在前面給出的典型回答,涵蓋了一些基本概念。如果基礎不牢,有些概念理解起來就比較晦澀,我建議還是盡量理解和掌握,即使有不懂的也不用擔心,在后續學習中還會逐步加深認識。
我個人認為,能夠基礎性地理解這些概念和機制,其實對于大多數并發編程已經足夠了,畢竟大部分工程師未必會進行更底層、更基礎的研發,很多時候解決的是知道與否,真正的提高還要靠實踐踩坑。
后面我會進一步分析:
* 從源碼層面,稍微展開一些 synchronized 的底層實現,并補充一些上面答案中欠缺的細節,有同學反饋這部分容易被問到。如果你對 Java 底層源碼有興趣,但還沒有找到入手點,這里可以成為一個切入點。
* 理解并發包中 java.util.concurrent.lock 提供的其他鎖實現,畢竟 Java 可不是只有 ReentrantLock 一種顯式的鎖類型,我會結合代碼分析其使用。
## 知識擴展
我在[上一講](http://time.geekbang.org/column/article/8799)提到過 synchronized 是 JVM 內部的 Intrinsic Lock,所以偏斜鎖、輕量級鎖、重量級鎖的代碼實現,并不在核心類庫部分,而是在 JVM 的代碼中。
Java 代碼運行可能是解釋模式也可能是編譯模式(如果不記得,請復習[專欄第 1 講](http://time.geekbang.org/column/article/6845)),所以對應的同步邏輯實現,也會分散在不同模塊下,比如,解釋器版本就是:
[src/hotspot/share/interpreter/interpreterRuntime.cpp](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/interpreter/interpreterRuntime.cpp)
為了簡化便于理解,我這里會專注于通用的基類實現:
[src/hotspot/share/runtime/](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/)
另外請注意,鏈接指向的是最新 JDK 代碼庫,所以可能某些實現與歷史版本有所不同。
首先,synchronized 的行為是 JVM runtime 的一部分,所以我們需要先找到 Runtime 相關的功能實現。通過在代碼中查詢類似“monitor\_enter”或“Monitor Enter”,很直觀的就可以定位到:
* [sharedRuntime.cpp](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/sharedRuntime.cpp)/hpp,它是解釋器和編譯器運行時的基類。
* [synchronizer.cpp](https://hg.openjdk.java.net/jdk/jdk/file/896e80158d35/src/hotspot/share/runtime/synchronizer.cpp)/hpp,JVM 同步相關的各種基礎邏輯。
在 sharedRuntime.cpp 中,下面代碼體現了 synchronized 的主要邏輯。
~~~
Handle h_obj(THREAD, obj);
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, lock, CHECK);
}
~~~
其實現可以簡單進行分解:
* UseBiasedLocking 是一個檢查,因為,在 JVM 啟動時,我們可以指定是否開啟偏斜鎖。
偏斜鎖并不適合所有應用場景,撤銷操作(revoke)是比較重的行為,只有當存在較多不會真正競爭的 synchronized 塊兒時,才能體現出明顯改善。實踐中對于偏斜鎖的一直是有爭議的,有人甚至認為,當你需要大量使用并發類庫時,往往意味著你不需要偏斜鎖。從具體選擇來看,我還是建議需要在實踐中進行測試,根據結果再決定是否使用。
還有一方面是,偏斜鎖會延緩 JIT 預熱的進程,所以很多性能測試中會顯式地關閉偏斜鎖,命令如下:
~~~
-XX:-UseBiasedLocking
~~~
* fast\_enter 是我們熟悉的完整鎖獲取路徑,slow\_enter 則是繞過偏斜鎖,直接進入輕量級鎖獲取邏輯。
那么 fast\_enter 是如何實現的呢?同樣是通過在代碼庫搜索,我們可以定位到 synchronizer.cpp。 類似 fast\_enter 這種實現,解釋器或者動態編譯器,都是拷貝這段基礎邏輯,所以如果我們修改這部分邏輯,要保證一致性。這部分代碼是非常敏感的,微小的問題都可能導致死鎖或者正確性問題。
~~~
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter(obj, lock, THREAD);
}
~~~
我來分析下這段邏輯實現:
* [biasedLocking](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/biasedLocking.cpp)定義了偏斜鎖相關操作,revoke\_and\_rebias 是獲取偏斜鎖的入口方法,revoke\_at\_safepoint 則定義了當檢測到安全點時的處理邏輯。
* 如果獲取偏斜鎖失敗,則進入 slow\_enter。
* 這個方法里面同樣檢查是否開啟了偏斜鎖,但是從代碼路徑來看,其實如果關閉了偏斜鎖,是不會進入這個方法的,所以算是個額外的保障性檢查吧。
另外,如果你仔細查看[synchronizer.cpp](https://hg.openjdk.java.net/jdk/jdk/file/896e80158d35/src/hotspot/share/runtime/synchronizer.cpp)里,會發現不僅僅是 synchronized 的邏輯,包括從本地代碼,也就是 JNI,觸發的 Monitor 動作,全都可以在里面找到(jni\_enter/jni\_exit)。
關于[biasedLocking](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/biasedLocking.cpp)的更多細節我就不展開了,明白它是通過 CAS 設置 Mark Word 就完全夠用了,對象頭中 Mark Word 的結構,可以參考下圖:

順著鎖升降級的過程分析下去,偏斜鎖到輕量級鎖的過程是如何實現的呢?
我們來看看 slow\_enter 到底做了什么。
~~~
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
if (mark->is_neutral()) {
// 將目前的 Mark Word 復制到 Displaced Header 上
lock->set_displaced_header(mark);
// 利用 CAS 設置對象的 Mark Word
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
TEVENT(slow_enter: release stacklock);
return;
}
// 檢查存在競爭
} else if (mark->has_locker() &&
THREAD->is_lock_owned((address)mark->locker())) {
// 清除
lock->set_displaced_header(NULL);
return;
}
// 重置 Displaced Header
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD,
obj(),
inflate_cause_monitor_enter)->enter(THREAD);
}
~~~
請結合我在代碼中添加的注釋,來理解如何從試圖獲取輕量級鎖,逐步進入鎖膨脹的過程。你可以發現這個處理邏輯,和我在這一講最初介紹的過程是十分吻合的。
* 設置 Displaced Header,然后利用 cas\_set\_mark 設置對象 Mark Word,如果成功就成功獲取輕量級鎖。
* 否則 Displaced Header,然后進入鎖膨脹階段,具體實現在 inflate 方法中。
今天就不介紹膨脹的細節了,我這里提供了源代碼分析的思路和樣例,考慮到應用實踐,再進一步增加源代碼解讀意義不大,有興趣的同學可以參考我提供的[synchronizer.cpp](hg.openjdk.java.net/jdk/jdk/file/896e80158d35/src/hotspot/share/runtime/synchronizer.cpp)鏈接,例如:
* **deflate\_idle\_monitors**是分析**鎖降級**邏輯的入口,這部分行為還在進行持續改進,因為其邏輯是在安全點內運行,處理不當可能拖長 JVM 停頓(STW,stop-the-world)的時間。
* fast\_exit 或者 slow\_exit 是對應的鎖釋放邏輯。
前面分析了 synchronized 的底層實現,理解起來有一定難度,下面我們來看一些相對輕松的內容。 我在上一講對比了 synchronized 和 ReentrantLock,Java 核心類庫中還有其他一些特別的鎖類型,具體請參考下面的圖。

你可能注意到了,這些鎖竟然不都是實現了 Lock 接口,ReadWriteLock 是一個單獨的接口,它通常是代表了一對兒鎖,分別對應只讀和寫操作,標準類庫中提供了再入版本的讀寫鎖實現(ReentrantReadWriteLock),對應的語義和 ReentrantLock 比較相似。
StampedLock 竟然也是個單獨的類型,從類圖結構可以看出它是不支持再入性的語義的,也就是它不是以持有鎖的線程為單位。
為什么我們需要讀寫鎖(ReadWriteLock)等其他鎖呢?
這是因為,雖然 ReentrantLock 和 synchronized 簡單實用,但是行為上有一定局限性,通俗點說就是“太霸道”,要么不占,要么獨占。實際應用場景中,有的時候不需要大量競爭的寫操作,而是以并發讀取為主,如何進一步優化并發操作的粒度呢?
Java 并發包提供的讀寫鎖等擴展了鎖的能力,它所基于的原理是多個讀操作是不需要互斥的,因為讀操作并不會更改數據,所以不存在互相干擾。而寫操作則會導致并發一致性的問題,所以寫線程之間、讀寫線程之間,需要精心設計的互斥邏輯。
下面是一個基于讀寫鎖實現的數據結構,當數據量較大,并發讀多、并發寫少的時候,能夠比純同步版本凸顯出優勢。
~~~
public class RWSample {
private final Map<String, String> m = new TreeMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public String get(String key) {
r.lock();
System.out.println(" 讀鎖鎖定!");
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String put(String key, String entry) {
w.lock();
System.out.println(" 寫鎖鎖定!");
try {
return m.put(key, entry);
} finally {
w.unlock();
}
}
// …
}
~~~
在運行過程中,如果讀鎖試圖鎖定時,寫鎖是被某個線程持有,讀鎖將無法獲得,而只好等待對方操作結束,這樣就可以自動保證不會讀取到有爭議的數據。
讀寫鎖看起來比 synchronized 的粒度似乎細一些,但在實際應用中,其表現也并不盡如人意,主要還是因為相對比較大的開銷。
所以,JDK 在后期引入了 StampedLock,在提供類似讀寫鎖的同時,還支持優化讀模式。優化讀基于假設,大多數情況下讀操作并不會和寫操作沖突,其邏輯是先試著讀,然后通過 validate 方法確認是否進入了寫模式,如果沒有進入,就成功避免了開銷;如果進入,則嘗試獲取讀鎖。請參考我下面的樣例代碼。
~~~
public class StampedSample {
private final StampedLock sl = new StampedLock();
void mutate() {
long stamp = sl.writeLock();
try {
write();
} finally {
sl.unlockWrite(stamp);
}
}
Data access() {
long stamp = sl.tryOptimisticRead();
Data data = read();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
data = read();
} finally {
sl.unlockRead(stamp);
}
}
return data;
}
// …
}
~~~
注意,這里的 writeLock 和 unLockWrite 一定要保證成對調用。
你可能很好奇這些顯式鎖的實現機制,Java 并發包內的各種同步工具,不僅僅是各種 Lock,其他的如[Semaphore](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/Semaphore.html)、[CountDownLatch](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/CountDownLatch.html),甚至是早期的[FutureTask](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/FutureTask.html)等,都是基于一種[AQS](https://docs.oracle.com/javase/10/docs/api/java/util/concurrent/locks/AbstractQueuedSynchronizer.html)框架。
今天,我全面分析了 synchronized 相關實現和內部運行機制,簡單介紹了并發包中提供的其他顯式鎖,并結合樣例代碼介紹了其使用方法,希望對你有所幫助。
## 一課一練
關于今天我們討論的你做到心中有數了嗎?思考一個問題,你知道“自旋鎖”是做什么的嗎?它的使用場景是什么?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?