2006 年以前,Java 工程師圈子裡流傳一條沒寫下來的潛規則:能不用 synchronized 就別用。不是它不安全,是它慢得讓人心痛。那個年代要在一段程式碼外面掛一把 synchronized,代價幾乎等於「每次都去敲作業系統的門」。

想像一下這個畫面。你在公司要用會議室,規定是:不管有沒有人跟你搶,每次都得先打電話給總機登記、等總機回你一個號碼,用完再打一次電話銷號。就算整層樓只有你一個人上班,這通電話也躲不掉。早期的 synchronized 就是這樣運作的——它背後直接綁著作業系統的互斥量(mutex)。執行緒一旦拿不到鎖,就得從使用者態切進核心態,被掛起、然後等別人放鎖時再被喚醒。

這個「切一趟核心態」的動作有多貴?一次執行緒的掛起與喚醒,成本大概落在一到數微秒之間。聽起來很短,但對 CPU 來說,一微秒可以跑掉好幾千條指令。你只是想保護一個小小的計數器加一,結果光是搶鎖的手續費就燒掉幾千條指令的預算。這就是為什麼那條潛規則會存在。

大家是怎麼繞過去的

既然正門這麼貴,工程師就開始找旁門。JDK 5 那年,Doug Lea 把他寫的併發工具包正式收進標準函式庫,也就是後來大家天天在用的 java.util.concurrentReentrantLockAtomicInteger 這些東西冒出來,背後靠的是 CAS(compare-and-swap)這種硬體層級的原子指令,不用驚動作業系統就能完成一次「檢查再更新」。

於是社群裡又長出新的信仰:synchronized 是老古董,講究效能就該改用 Lock。這個說法在當時是對的。但它有個副作用——大家把「synchronized 慢」當成永恆真理背了下來,卻沒注意到 JVM 的人根本不打算讓這個標籤黏一輩子。

轉捩點:讓鎖學會看臉色

真正的翻盤發生在 JDK 6。JVM 團隊沒有去改 synchronized 的語意,你的程式碼一個字都不用動,他們改的是這把鎖「底下」的實作。核心洞察其實很樸素:大部分的鎖,根本沒人在搶。

先別急著往下看,先停下來想一個問題。你程式裡那些 synchronized,有多少是真的有兩條執行緒在同一瞬間硬碰硬?答案通常是:極少。很多鎖從頭到尾都只有同一條執行緒在進出,或者就算有多條,也是你用完我才用,錯開的。既然如此,為什麼要每一次都付「搶鎖」的全額手續費?

JDK 6 的答案是讓鎖分等級,依照競爭的激烈程度一級一級往上爬,這就是鎖升級。回到會議室那個比喻,新規則變成三段:

第一段是偏向鎖。第一個進來的人,系統直接把他的名字貼在門上。之後只要是同一個人進出,看一眼門上是自己的名字就走進去,完全不用打電話、不用登記,連 CAS 都省了。它「偏心」地假設這間房以後都是你在用。對「一條執行緒反覆進出同一把鎖」的場景,這幾乎把成本壓到了零。

第二段是輕量級鎖。哪天真的來了第二個人也想用,偏向鎖就得先撤銷,換成另一種玩法:想進門的人不去找總機排隊,而是站在門口用 CAS 反覆問「現在能進了嗎」,這個動作叫自旋。競爭不激烈的時候,通常自旋轉個幾圈,前一個人就出來了,你直接接手,始終沒驚動作業系統。

第三段才是我們開頭那把老鎖——重量級鎖。如果門口的人越站越多、自旋轉了半天還進不去,JVM 判斷這是真的塞車了,才升級成傳統那套:回頭找總機、乖乖被掛起、排隊叫號。這時候你付的就是最貴的核心態切換,但這筆錢花得值,因為競爭已經激烈到不掛起反而更浪費 CPU。

這些狀態記在哪裡

這裡值得把黑盒再打開一層。鎖現在在哪一級,這個資訊得存在某個地方——它就藏在每個 Java 物件的物件頭裡,一塊叫 Mark Word 的區域。64 位元的 JVM 上,這塊大約就是 64 個位元的空間,平常它拿來放物件的 hashCode、GC 的分代年齡;一旦這個物件被拿來當鎖,同一塊空間就改用來記鎖的標誌位、以及偏向了哪條執行緒的 ID。

看懂這件事,很多細節就順了。為什麼一個物件你先呼叫了 hashCode()、它就沒辦法再進偏向鎖?因為那塊位置被 hashCode 佔走了,沒地方記執行緒 ID。為什麼鎖只升不降?因為降級要處理的邊界狀況太多,划不來。JVM 的設計者在這裡做的,就是拿有限的幾十個位元,去換掉那幾千條指令的手續費——這是一筆很典型的空間換時間的交易。

故事還沒結束

如果這篇停在 JDK 6,結論會是「偏向鎖萬歲」。但時間再往前走,2020 年的 JDK 15 做了一件很多人意外的事:透過 JEP 374,偏向鎖被預設關掉、標記為 deprecated,後續版本還把相關程式碼一步步拆掉。

當年的救世主,怎麼十幾年後反而被請下台?原因很現實。偏向鎖有個隱藏成本——撤銷。當偏向假設被打破(第二個執行緒出現),那次撤銷本身要停頓、要協調,並不便宜。JDK 6 的時代,應用大多是少數執行緒長時間持有鎖,偏向命中率高,這筆撤銷成本攤下來划算。但現代的服務用了大量執行緒池、任務在執行緒之間頻繁搬移,「同一條執行緒反覆拿同一把鎖」的假設越來越少成立,偏向鎖猜錯的次數變多,撤銷的帳越滾越大,再加上它讓 JVM 內部維護變複雜,最後 JVM 團隊算了一筆總帳,決定收掉它。

所以那條 2006 年的老潛規則,現在該怎麼更新?其實它早就不成立了——今天你隨手寫下的 synchronized,在沒有競爭時靠輕量級鎖的一次 CAS 就搞定,再加上 JIT 編譯器還會在背後幫你做鎖消除、鎖粗化,把根本沒必要的鎖直接拿掉。它早就不是那個要你敲作業系統門的老古董了。

下一個問題留給你自己去追:既然偏向鎖走了,少了那層「幾乎零成本」的最快路徑,JVM 是靠什麼把這塊效能補回來的?這條時間線,顯然還沒畫完。