
1. 從一次線上事故說起為什么我們需要重新審視Synchronized那天下午系統監控突然報警核心交易接口的響應時間從平均50毫秒飆升至5秒以上緊接著就是一連串的“調用超時”錯誤。我們緊急排查發現罪魁禍首是一個看似簡單的庫存扣減方法它被Transactional注解包裹內部又用synchronized方法進行了“雙重保險”。在低并發下相安無事一旦流量起來這個“保險”就成了性能瓶頸大量線程在等待同一個鎖數據庫連接池也被迅速耗盡。我們移除了synchronized改用數據庫行鎖配合重試機制問題才得以解決。這次事故讓我意識到盡管synchronized是Java開發者最早接觸、最“順手”的鎖但很多人對它的理解仍停留在“加上就線程安全了”的層面。它背后的鎖升級過程、與JVM內存模型的交互、以及在現代高并發架構中的適用邊界遠比我們想象的要復雜。今天我就結合自己踩過的坑和源碼層面的理解把synchronized這把“老鎖”掰開揉碎了講清楚這可能是你能看到的關于它最細致的一篇解讀。2. Synchronized的“三重面孔”語法、語義與底層實現很多人覺得synchronized用法簡單無非是修飾方法或代碼塊。但它的每一種用法都對應著JVM完全不同的處理邏輯和鎖對象理解這點是避免誤用的第一步。2.1 三種使用方式及其鎖對象1. 實例方法同步這是最常見的形式。當你用synchronized修飾一個非靜態方法時鎖住的是當前調用該方法的對象實例this。public class Counter { private int count 0; public synchronized void increment() { count; // 鎖對象是 this } }這意味著如果兩個線程試圖在同一個Counter對象上調用increment()它們會互斥。但如果它們操作的是兩個不同的Counter對象則不會發生鎖競爭因為鎖對象不同。我見過有團隊在Spring管理的單例Service類上濫用實例方法同步導致整個應用的所有相關請求串行化性能慘不忍睹。2. 靜態方法同步當synchronized修飾靜態方法時鎖住的是當前類的Class對象。public class StaticCounter { private static int count 0; public static synchronized void increment() { count; // 鎖對象是 StaticCounter.class } }這個鎖的粒度非常大因為一個JVM中一個類的Class對象是唯一的。無論你創建多少個StaticCounter實例或者從哪個線程調用所有對靜態increment()方法的訪問都是互斥的。它通常用于保護靜態變量但必須慎用否則極易成為全局性能瓶頸。3. 同步代碼塊這是最靈活也最能體現你對鎖范圍控制能力的方式。你需要顯式指定一個對象作為鎖monitor。public class FineGrainedCounter { private final Object lock new Object(); // 專門的鎖對象 private int countA 0; private int countB 0; public void incA() { // 只鎖與countA相關的操作 synchronized (lock) { countA; } // 其他不需要同步的操作可以放在外面 doSomethingElse(); } public void incB() { // 如果countB和countA完全獨立甚至可以用另一個鎖對象 // 比如 private final Object lockB new Object(); synchronized (lock) { countB; } } }使用代碼塊的關鍵在于鎖對象的選擇。鎖對象通常應該是private final的防止外部代碼意外獲得你的鎖導致死鎖或性能問題。絕對不要使用可能會被修改的對象如String字面量有常量池問題或基礎類型的包裝類作為鎖。2.2 鎖的本質對象頭與Monitorsynchronized的鎖信息存儲在哪里答案是Java對象的對象頭Object Header里。一個普通的Java對象在堆內存中的布局可以分為三部分對象頭、實例數據和對齊填充。其中對象頭又包含Mark Word存儲對象的哈希碼、GC分代年齡、鎖狀態標志等。這是實現synchronized的關鍵。Klass Pointer指向對象元數據的指針。如果是數組數組長度。在32位JVM上Mark Word是32位64位JVM上是64位。為了在有限的空間里存儲大量信息Mark Word的設計是“動態”的它的位模式會根據對象的狀態是否被鎖定、是否被偏向等而改變。當線程進入synchronized塊時JVM需要關聯一個Monitor管程對象來管理鎖的競爭與等待。每個Java對象天生都關聯著一個潛在的Monitor。你可以把Monitor想象成一個房間臨界區它有一個所有者持有鎖的線程一個入口隊列Entry Set競爭鎖的線程在此排隊和一個等待隊列Wait Set調用wait()的線程在此等待。synchronized的加鎖過程本質上就是線程通過CAS操作去競爭對象Mark Word中指向的Monitor的所有權。成功則獲取鎖失敗則進入阻塞隊列。3. 鎖的升級與優化從偏向鎖到重量級鎖早期的synchronized是純粹的“重量級鎖”依賴操作系統底層的互斥量Mutex Lock實現涉及用戶態到內核態的切換開銷巨大。為了提升在無競爭或低競爭場景下的性能HotSpot虛擬機從Java 6開始實現了鎖升級Lock Coarsening機制。鎖的狀態不再是固定的而是根據競爭情況動態變化主要經歷四個階段無鎖 - 偏向鎖 - 輕量級鎖 - 重量級鎖。這個升級過程是不可逆的偏向鎖可以被撤銷回到無鎖理解這個過程對于性能調優至關重要。3.1 偏向鎖單線程的“特權”設計初衷在大多數情況下鎖不僅不存在多線程競爭而且總是由同一線程多次獲得。為了讓線程獲得鎖的代價更低引入了偏向鎖。工作原理加鎖當一個線程第一次訪問同步塊時JVM會使用CAS操作將線程ID記錄到對象頭的Mark Word中并將鎖標志位設置為“01”偏向模式。之后這個線程再進入和退出同步塊時不需要進行任何同步操作如CAS只需簡單檢查Mark Word里是否存儲著自己的線程ID。撤銷一旦有另一個線程來嘗試競爭這個鎖偏向模式就宣告結束。持有偏向鎖的線程必須將鎖撤銷Revoke Bias。撤銷過程需要等待全局安全點即所有線程都暫停執行然后檢查原持有線程是否還活著或已退出同步塊。如果已退出則將對象頭恢復為無鎖狀態允許新線程競爭如果還在同步塊內則升級為輕量級鎖。適用場景與坑點 偏向鎖適用于明確知道只有一個線程會頻繁訪問的同步場景。但在高并發、鎖競爭激烈的環境下偏向鎖的撤銷操作尤其是需要STW的撤銷會帶來額外的性能損耗。因此在JDK 15之后偏向鎖被默認禁用-XX:-UseBiasedLocking并且在后續版本中被標記為廢棄。對于新的應用尤其是微服務架構我通常建議在JVM參數中顯式關閉偏向鎖。3.2 輕量級鎖線程間交替執行的“禮貌競爭”當偏向鎖被撤銷或者一開始就關閉了偏向鎖線程會嘗試使用輕量級鎖。工作原理加鎖在代碼即將進入同步塊時如果鎖對象處于無鎖狀態JVM會在當前線程的棧幀中創建一個名為鎖記錄Lock Record的空間用于存儲鎖對象當前的Mark Word拷貝稱為Displaced Mark Word。然后JVM使用CAS操作嘗試將對象頭的Mark Word更新為指向該鎖記錄的指針。如果成功當前線程獲得鎖并將鎖標志位設置為“00”輕量級鎖狀態。如果失敗說明至少存在兩條線程在競爭同一個鎖會先進行自旋。自旋競爭失敗的線程不會立即阻塞而是進行一個循環自旋不斷地檢查鎖是否被釋放。自旋的目的是為了避免線程切換用戶態到內核態的開銷。解鎖解鎖時同樣使用CAS操作將Displaced Mark Word替換回對象頭。如果成功則表示沒有競爭發生如果失敗說明在持有鎖期間鎖已經升級為重量級鎖此時需要喚醒被阻塞的線程。適用場景與坑點 輕量級鎖適用于線程交替執行同步塊即鎖競爭是“短暫”且“稀疏”的場景。它的性能開銷在于CAS操作和自旋消耗的CPU周期。注意自旋并非無限進行。JVM采用了適應性自旋Adaptive Spinning策略會根據之前自旋的成功歷史動態調整自旋次數。如果自旋始終無法獲得鎖或者自旋線程數過多超過CPU核心數的一半為了避免CPU空轉鎖會膨脹為重量級鎖。3.3 重量級鎖真正的“排他”與阻塞當輕量級鎖競爭失敗自旋失敗鎖就會膨脹為重量級鎖。工作原理 此時對象頭的Mark Word中存儲的是指向重量級鎖Monitor對象的指針。其余所有試圖獲取鎖的線程都會進入阻塞BLOCKED狀態。鎖的獲取和釋放完全依賴操作系統底層的互斥量Mutex和條件變量Condition Variable涉及用戶態到內核態的切換、線程的掛起和喚醒開銷最大。適用場景 這是synchronized的“保底”機制適用于高并發、長時間持有鎖、競爭激烈的場景。雖然開銷大但它能保證在極端情況下的正確性和公平性操作系統調度器會管理阻塞隊列。3.4 鎖升級流程圖與實戰意義我們可以用一個簡單的流程圖來概括這個動態過程線程嘗試進入同步塊 | v 檢查對象Mark Word鎖標志位 | v [無鎖/可偏向] --(偏向鎖開啟且未偏向)-- [嘗試CAS偏向當前線程] -- 成功 -- [偏向鎖] | | | 失敗/發生競爭 v v [偏向鎖] --(其他線程競爭)-- [撤銷偏向升級為輕量級鎖] | v [輕量級鎖] --(CAS獲取鎖)-- 成功 -- [持有輕量級鎖] | | | 失敗自旋失敗/多線程競爭 v v [重量級鎖] ------------------實戰意義不要為了“性能”而盲目使用synchronized在極低競爭或無競爭場景它確實經過優化。但在你無法預知競爭程度的共享資源上它可能退化為性能殺手。鎖對象的作用域至關重要鎖住一個成員變量和鎖住整個this對象競爭范圍天差地別。盡量減小同步塊的范圍。理解“鎖粗化”和“鎖消除”這是JIT編譯器的另外兩項優化。鎖粗化如果虛擬機探測到有一串零碎的操作都對同一個對象加鎖解鎖會把鎖同步的范圍擴展粗化到整個操作序列的外部減少加鎖解鎖次數。鎖消除如果JIT編譯器通過逃逸分析發現某個鎖對象不可能被其他線程訪問到即不會發生共享那么就會將這個鎖消除。例如在方法內部創建的、僅用于同步的局部對象。4. Synchronized與Java內存模型可見性與有序性的保證synchronized不僅僅是互斥鎖它還是Java內存模型JMM中一個重要的內存屏障Memory Barrier。它能解決并發編程中的三大問題原子性、可見性、有序性。原子性由鎖的互斥性自然保證同步塊內的操作作為一個不可分割的整體執行??梢娦愿鶕﨡MM規范對一個鎖的解鎖unlock操作happens-before于后續對這個鎖的加鎖lock操作。這意味著線程A在synchronized塊內修改了共享變量在解鎖后線程B在進入同一個鎖保護的synchronized塊時一定能看到線程A修改后的最新值。這是因為解鎖時JVM會將線程本地內存工作內存中的變量刷新到主內存加鎖時會清空本地內存從主內存重新加載變量。有序性synchronized塊內的代碼雖然可能被編譯器或處理器重排序但由于“as-if-serial”語義和內存屏障從其他線程的視角來看其執行結果與程序順序執行的結果一致。同時它禁止了塊內指令與塊外指令的某些重排序保證了臨界區操作的順序性。一個常見的誤解有人認為volatile能替代synchronized實現同步。volatile只保證了可見性和禁止指令重排序部分有序性但不保證復合操作的原子性。例如count這樣的“讀-改-寫”操作即使count是volatile的在多線程下仍然會出錯因為它不是原子操作。而synchronized可以保證整個count操作的原子性。5. 深入對比Synchronized vs LockReentrantLock“既然有java.util.concurrent.locks.Lock接口和功能強大的ReentrantLock為什么還要用synchronized”這是面試??碱}也是技術選型的關鍵。特性synchronizedReentrantLock實現層面JVM原生支持關鍵字JDK API層面實現類鎖的獲取隱式獲取和釋放進入塊自動獲取退出正?;虍惓W詣俞尫棚@式調用lock()和unlock()必須在finally塊中釋放靈活性較差鎖的釋放由JVM控制很強可嘗試非阻塞獲取(tryLock)、可中斷獲取(lockInterruptibly)、超時獲取公平性非公平鎖但內部實現有適應性的優化可選公平鎖或非公平鎖構造參數指定條件變量通過Object.wait(),notify(),notifyAll()一個鎖對應一個等待隊列通過Condition接口一個鎖可以綁定多個Condition實現更精細的線程等待/喚醒性能Java 6后大幅優化在低至中等競爭下與Lock相差無幾甚至更好在高競爭場景下由于其更復雜的邏輯和API開銷可能略遜一籌但功能優勢明顯調試獲取鎖的堆棧信息在JDK工具中可能不直觀可以通過getHoldCount(),getQueueLength()等方法獲取鎖狀態信息便于調試選型建議個人經驗優先使用synchronized除非你需要ReentrantLock提供的高級功能如可中斷、超時、公平鎖、多個條件隊列。synchronized的簡潔性和不易出錯自動釋放是巨大優勢。大多數業務場景它的性能已經足夠好。考慮使用ReentrantLock需要可定時的、可輪詢的鎖獲取tryLock(long time, TimeUnit unit)比如在死鎖恢復場景。需要可中斷的鎖獲取lockInterruptibly()讓等待鎖的線程能響應中斷。需要公平鎖雖然通常性能較差。需要綁定多個條件變量Condition實現復雜的線程協作如生產者-消費者模型中的多個等待隊列。性能不是首要考慮因素在絕大多數應用里鎖競爭本身才是瓶頸而不是synchronized和ReentrantLock之間的細微性能差異。首先應該優化的是鎖的粒度和持有鎖的時間。6. 實戰避坑指南與性能優化理論懂了還得在實戰中不踩坑。下面是我總結的幾個關鍵點和優化技巧。6.1 鎖對象選擇不當引發的“神秘”問題案例使用字符串常量作為鎖。private static final String LOCK “LOCK”; public void method() { synchronized(LOCK) { // 危險 // ... } }問題字符串常量具有駐留intern特性。“LOCK”在JVM字符串常量池中是唯一的。這意味著如果你的代碼其他模塊甚至是引用的第三方庫也鬼使神差地用“LOCK”這個字符串作為鎖就會導致意料之外的鎖競爭引發死鎖或性能問題且極難排查。正確做法使用專門創建的、不可變的對象作為鎖。private final Object lock new Object(); // 實例鎖 private static final Object STATIC_LOCK new Object(); // 類鎖6.2 死鎖的經典場景與排查synchronized嵌套使用不當是死鎖的溫床。// 線程1 synchronized (lockA) { Thread.sleep(100); // 模擬業務操作增加死鎖概率 synchronized (lockB) { // ... } } // 線程2 synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { // 死鎖發生 // ... } }排查與預防避免嵌套鎖盡量設計成只獲取一把鎖。如果必須嵌套確保所有線程以相同的全局順序獲取鎖例如都按lockA-lockB的順序。使用帶超時的鎖如果使用ReentrantLock可以用tryLock(timeout)。使用JDK工具jstack pid可以打印線程堆棧能清晰看到哪些線程在等待哪些鎖是定位死鎖的首選工具。6.3 鎖粒度控制細粒度鎖與鎖分段對于保護一個組合對象如HashMap鎖住整個對象synchronized(map)粒度太粗??梢圆捎酶毩6鹊牟呗?。示例簡易鎖分段public class StripedMap { private final int N_LOCKS 16; private final Node[] buckets; private final Object[] locks; public StripedMap(int capacity) { buckets new Node[capacity]; locks new Object[N_LOCKS]; for (int i 0; i N_LOCKS; i) { locks[i] new Object(); } } private final int hash(Object key) { return Math.abs(key.hashCode() % buckets.length); } public Object get(Object key) { int hash hash(key); // 只鎖住這個桶對應的鎖而不是整個map synchronized (locks[hash % N_LOCKS]) { for (Node m buckets[hash]; m ! null; m m.next) { if (m.key.equals(key)) { return m.value; } } } return null; } // ... put方法類似 static class Node { Object key, value; Node next; } }ConcurrentHashMap在JDK 7及之前就采用了分段鎖Segment正是這種思想的體現。在JDK 8中它改為使用synchronized鎖住單個桶鏈表頭或紅黑樹根節點 CAS實現了更細粒度的并發控制。6.4 性能監控與診斷如何知道你的synchronized成了瓶頸JVisualVM / JMC監控線程狀態如果大量線程長時間處于BLOCKED狀態很可能存在鎖競爭。Arthas使用thread -b命令可以一鍵找出當前阻塞其他線程最多的“罪魁禍首”線程。自定義監控對于關鍵鎖可以簡單包裝一下記錄持有時間、等待時間等。public class MonitoredSync { private final Object lock new Object(); private long totalWaitTime 0; private long holdCount 0; public void doSomething() throws InterruptedException { long startWait System.nanoTime(); synchronized (lock) { long waitTime System.nanoTime() - startWait; totalWaitTime waitTime; holdCount; // 實際業務邏輯 Thread.sleep(10); // 模擬工作 } // 可以定期打印或上報平均等待時間 totalWaitTime / holdCount } }7. 在現代架構中的定位何時用何時棄在分布式、高并發的今天synchronized的戰場主要收縮到了單JVM進程內的線程同步。適用場景單機多線程資源保護如內存中的計數器、緩存、連接池狀態等。Spring單例Bean的方法同步需謹慎評估范圍。實現簡單的線程安全工具類如懶加載的單例模式雙重檢查鎖定中實例變量需用volatile修飾。作為更復雜并發組件如ConcurrentHashMap在JDK8中的實現的基礎構建塊。不適用/需要替代的場景分布式環境下的共享資源如分布式庫存、分布式ID生成。這里需要分布式鎖如基于RedisRedisson、ZooKeeper或數據庫的實現。需要更復雜協調機制的場景如多個條件隊列、可中斷鎖等使用ReentrantLock和Condition。超高并發且臨界區極短的場景可以考慮使用java.util.concurrent.atomic包下的原子變量它們基于CAS實現在無競爭或低競爭時性能通常優于鎖。讀寫比例極高的場景使用ReadWriteLock或StampedLock實現讀寫分離允許多個讀線程同時訪問。最后一點體會synchronized就像一把瑞士軍刀中的主刀簡單、可靠、在大多數日常場景下夠用。但作為一名專業的開發者你必須清楚知道它何時會變鈍性能瓶頸何時根本不適用分布式場景以及工具箱里還有哪些更專業的工具Lock、原子類、并發容器可供選擇。深入理解其原理是為了更精準、更自信地使用它而不是盲目地回避或濫用。在微服務架構下我越來越少在業務代碼中直接使用synchronized更多的并發控制被轉移到了數據庫的事務隔離級別、Redis的分布式鎖或無鎖的數據結構上但它在框架底層和基礎組件中依然扮演著不可或缺的角色。