synchronized 還是 ReentrantLock?先問你有沒有這四種需求
先回答一個問題:你程式碼裡有沒有哪一段,等鎖等太久就應該放棄,去走別的路?
想不出來的話,這篇後面的內容你可以不用看。繼續用 synchronized,你沒有損失任何東西。
ReentrantLock 在很多團隊裡被當成「比較進階的鎖」。review 的時候看到有人寫 synchronized,還會有人留言問要不要換成 lock。這個直覺是錯的,而且錯得有點久了。
名字騙了你,可重入從來不是賣點
ReentrantLock 這個名字翻成中文是「可重入鎖」。可重入的意思是:同一個執行緒已經拿到這把鎖了,再進來一次不會把自己卡死。
聽起來很厲害。問題是 synchronized 也可重入。
1 | public synchronized void outer() { |
這段程式碼跑得好好的,JDK 1.0 時代就是這樣。所以如果你選 ReentrantLock 的理由是「它可以重入」,那你等於是為了一個免費的東西付了錢。
那名字為什麼要這樣取?因為它是在標示「這把鎖跟你已經熟悉的那把行為一致」,不是在標示差異。取名的人想講的是「別怕,語意跟 synchronized 一樣」,結果一堆人讀成「這是 synchronized 沒有的功能」。
真正的差異在別的地方,總共四條。
需求一:等不到就要走人
synchronized 只有一種等法,就是等到天荒地老。你沒有任何機會告訴 JVM「等 200 毫秒就好,拿不到我自己想辦法」。
ReentrantLock 有:
1 | private final ReentrantLock lock = new ReentrantLock(); |
這個差別在對外服務上是生死線。一個 API 如果搶不到鎖就無限期掛在那,佔住的不只是它自己那條執行緒,還有上游的連線。連線池被吃光之後,倒的不是這支 API,是所有共用那個池子的功能。有 timeout,最壞情況是變慢;沒 timeout,最壞情況是整片一起停。
而且這種故障最難查的地方在於,現場看起來完全沒有錯誤。沒有例外、沒有 stack trace,只有一堆執行緒安安靜靜地站在門口等,監控上只看得到延遲爬升。等你想到要去抓 thread dump,通常已經過了半小時。
順帶一提 tryLock() 那個不帶參數的版本有個坑:就算你建的是公平鎖,它照樣插隊。
1 | ReentrantLock fair = new ReentrantLock(true); // 說好要排隊的 |
Javadoc 寫得很白,無參數的 tryLock() 刻意設計成不遵守公平性。想要排隊語意,得用 tryLock(0, TimeUnit.SECONDS),帶超時參數的那個版本才會照隊伍走。這種事沒踩過不會知道。
需求二:等鎖的時候要能被叫回來
第二條跟第一條長得像,但解的是不同的問題。
執行緒卡在 synchronized 前面等鎖的時候,你對它 interrupt() 是沒有用的。它不理你,它就是要等到拿到為止。這代表如果這個任務被取消了、如果整個服務要優雅關機,這條執行緒你叫不動。
lockInterruptibly() 解掉這件事:
1 | public void handle(Task task) { |
用 ExecutorService 而且會呼叫 shutdownNow() 的服務,這條特別有感。shutdownNow() 做的事就是對所有 worker 送 interrupt,如果 worker 全卡在 synchronized 上,你那個「優雅關機」就只是名字好聽而已。
需求三:一個房間裡等的是兩種不同的人
一間只有一台叫號機的餐廳,內用要等位子,外帶要等餐點,兩種客人共用同一組號碼。廚房做好一份外帶餐按下叫號,整間店的人全部抬頭,發現不是叫自己,再坐回去。
synchronized 的 wait() / notifyAll() 就是這台機器。一把鎖只配一組等待佇列,你只能全部叫醒,讓大家自己判斷該不該醒。
ReentrantLock 可以生出多台叫號機:
1 | private final ReentrantLock lock = new ReentrantLock(); |
生產者只吵醒消費者,消費者只吵醒生產者,互不打擾。佇列一深、執行緒一多,notifyAll() 那種全場點名的做法會把 CPU 浪費在「醒來、發現不是我、再睡」這個循環上。這叫驚群效應(thundering herd),跟 spurious wakeup 是兩回事,後者指的是沒有任何人叫你、你也會自己醒過來。順帶一提,正因為 spurious wakeup 真的存在,等待條件才一定要包在 while 裡而不是 if。
不過話說回來,如果你正在手刻生產者消費者佇列,先停一下,去看看 ArrayBlockingQueue 是不是就夠用了。它內部做的就是上面那段程式碼,而且已經被幾億次生產環境跑過。自己刻的理由要比「想練習」更強一點。
需求四:這條是虛擬執行緒帶進來的,而且有時效性
前三條是 JDK 5 就有的老知識,任何一本併發書都寫過。第四條不一樣,它是虛擬執行緒(virtual thread)帶進來的,而且它會隨著你升版而失效。
虛擬執行緒的整套設計建立在一件事上:它阻塞的時候,要能從底下那條真正的作業系統執行緒(carrier thread)身上卸下來,讓別人先用。卸得掉,你才能開幾十萬條虛擬執行緒;卸不掉,那些 carrier thread 就被佔著,池子一乾就全體停擺。這個卸不掉的狀態叫 pinning。
而在 JDK 21 到 23,synchronized 區塊裡的阻塞正好就卸不掉。java.util.concurrent 那套鎖則不會有這個問題,所以官方當時給的建議就是把熱點路徑上的 synchronized 換成 ReentrantLock。
JEP 491 在 JDK 24 把這件事修掉了,synchronized 也能正常卸下,官方原文直接寫這個遷移「no longer necessary」。
但這裡有個很多人會漏掉的細節:JDK 24 不是 LTS。 最近一個內含這項修正的長期支援版本是 JDK 25,2025 年 9 月才 GA。也就是說,如果你的服務跟大多數團隊一樣還蹲在 JDK 21 LTS 上,又剛好用了虛擬執行緒,這條理由對你依然成立,而且會一直成立到你升上 25 為止。
還有一個配套要記。JEP 491 把 jdk.tracePinnedThreads 這個系統屬性拿掉了,設了也沒反應。要診斷 pin,改看 JFR 的 jdk.VirtualThreadPinned 事件,它現在會告訴你 pin 的原因和是哪條 carrier thread 被佔住。
換過去要付的代價:JVM 不再幫你收尾
上面講了四個該換的理由,現在講為什麼預設不該換。
synchronized 是語言層級的結構。程式碼跑出那個大括號,鎖就釋放,不管你是正常跑完、return 提早離開、還是拋了例外。這件事由 JVM 保證,你不用寫任何一行程式碼。
ReentrantLock 是一個普通的物件,JVM 不知道你在幹嘛。忘了 unlock(),那把鎖就永遠不會回來,後面所有要用的執行緒全部堆在門口。
1 | lock.lock(); |
所以 try / finally 不是風格建議,是強制規範。而且 lock() 要寫在 try 外面:寫在裡面的話,萬一 lock() 自己失敗了,finally 還是會去 unlock() 一把你根本沒拿到的鎖,直接送你一個 IllegalMonitorStateException。
這種「靠人記得」的東西在小專案裡沒事,在四十個人的 codebase 裡就是遲早會爆的定時炸彈。你可以靠 code review 擋,但 code review 會累,JVM 不會。
反過來說,這個缺點偶爾也是唯一解。synchronized 強制鎖跟解鎖必須在同一個區塊裡(Javadoc 用的字是 block-structured),而 ReentrantLock 允許你在 A 方法上鎖、在 B 方法解鎖。手刻鏈式鎖交接(hand-over-hand locking)之類的資料結構會需要這個,但那是很稀有的場景,正常業務程式碼碰到的機率接近零。
效能這個理由,早就過期了
還有一個常常被拿出來講的理由:ReentrantLock 比較快。
這句話在 2005 年是對的。那時 synchronized 每次都要跟作業系統要 mutex,代價高到工程師會刻意繞路。後來 JVM 陸續補上偏向鎖、輕量級鎖、自旋這些優化,低競爭情況下的差距被壓到幾乎看不出來。至於偏向鎖後來為什麼又被拿掉、鎖升級這條路是怎麼走完的,我在另一篇拆過完整的時間線。
現在的實務結論很簡單:低競爭下兩者差不多,高競爭下你該做的是把鎖的範圍縮小、或者乾脆想辦法不共享,而不是換一把鎖。真的想不共享,ThreadLocal 那條路通常比換鎖有效得多。
換句話說,把 ReentrantLock 當效能優化來用,是拿一個功能選項去解一個結構問題。
所以到底怎麼選
synchronized 像旅館房門,走出去門自己鎖上,你不用管。ReentrantLock 像你自己帶去健身房的掛鎖,能決定要不要等、能設鬧鐘、能一次管好幾排櫃子,代價是忘記鎖回去就是你家的事。
所以判準只有一句:預設寫 synchronized,直到你講得出上面四條裡的哪一條。 講不出來就別換,那不叫進階,那叫多背一個維護風險。
素材來源:個人 Notion 開發筆記(ReentrantLock 可重複取得鎖),內容經改寫與更新至 2026 年的 JDK 現況。


































