引入
在多線程編程的世界里,共享資源的訪問控制就像一場精心設計的交通管制,而Synchronized作為Java并發編程的基礎同步機制,扮演著"交通警察"的關鍵角色。
并發編程的核心矛盾
當多個線程同時訪問共享資源時,"線程安全"問題便應運而生。想象一個銀行賬戶的場景:若兩個線程同時執行扣款操作,可能導致賬戶余額出現負數或不一致的情況。這種情況下,我們需要一種機制來確保在同一時刻只有一個線程能操作共享資源,這就是同步鎖的核心使命。
Synchronized的歷史地位
作為Java語言內置的同步機制,Synchronized從JDK1.0時代便已存在。早期版本中,它因"重量級鎖"的標簽被認為性能不佳,但隨著JDK6之后的一系列優化(如偏向鎖、輕量級鎖的引入),其性能表現已大幅提升,在很多場景下甚至優于ReentrantLock
。
同步鎖的三大核心特性
-
互斥性:確保同一時刻只有一個線程獲取鎖并執行同步代碼
-
可見性:保證釋放鎖時對共享變量的修改能立即被其他線程看見
-
有序性:禁止指令重排序,確保同步代碼塊內的操作按順序執行
這些特性通過JVM底層的監視器鎖(Monitor)機制實現,是理解Synchronized的關鍵切入點。
鎖的狀態與類型:從無鎖到重量級鎖的演進
JVM視角下的鎖狀態體系
從JVM實現層面看,鎖的狀態可分為4種,每種狀態對應不同的競爭程度和性能特征:
狀態值 | 狀態名稱 | 競爭程度 | 典型場景 | Mark Word結構(64位JVM) |
---|---|---|---|---|
0 | 無鎖狀態 | 無競爭 | 單線程訪問 | 對象哈希碼(25bit) + GC分代年齡(4bit) + 鎖標志位(01) + 偏向鎖標志(0) |
1 | 偏向鎖狀態 | 輕微競爭 | 單線程重復訪問 | 線程ID(54bit) + GC分代年齡(4bit) + 鎖標志位(01) + 偏向鎖標志(1) |
2 | 輕量級鎖狀態 | 中度競爭 | 多線程自旋等待 | 指向棧中鎖記錄的指針(62bit) + 鎖標志位(00) |
3 | 重量級鎖狀態 | 高度競爭 | 多線程阻塞等待 | 指向監視器對象的指針(62bit) + 鎖標志位(10) |
鎖狀態轉換圖:
無鎖狀態 ? 偏向鎖狀態 ? 輕量級鎖狀態 ? 重量級鎖狀態↑ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ↓└────────────── 鎖膨脹 ────────────┘
這種狀態轉換是單向不可逆的,只能從低競爭狀態向高競爭狀態升級(鎖膨脹),而不能降級,這是JVM為了優化性能做出的設計選擇。
偏向鎖:單線程優化的利器
偏向鎖的核心思想
偏向鎖是JVM對"同一線程多次獲取同一鎖"場景的優化,它通過在對象頭中記錄線程ID的方式,避免重復獲取鎖的開銷。當一個線程首次獲取對象鎖時,JVM會將偏向鎖標志位設為1,并將線程ID寫入Mark Word,后續該線程再次訪問時無需進行CAS操作,直接判斷Mark Word中的線程ID是否與當前線程一致。
偏向鎖的激活與撤銷
-
激活條件:JVM參數
-XX:+UseBiasedLocking
(JDK6后默認啟用) -
撤銷場景:
-
當其他線程嘗試獲取偏向鎖時,會觸發偏向鎖撤銷
-
調用
wait()
/notify()
等方法時,偏向鎖會升級為輕量級鎖 -
偏向鎖可以通過
BiasedLockingStartupDelay
參數控制延遲激活時間
-
典型應用場景
偏向鎖最適合"單線程反復訪問同步資源"的場景,例如:
public class BiasedLockDemo {private Object lock = new Object();public void doWork() {synchronized (lock) {// 單線程頻繁執行的業務邏輯}}
}
在這種場景下,偏向鎖能消除幾乎所有的鎖獲取開銷。
輕量級鎖:自旋等待的藝術
輕量級鎖的實現原理
當偏向鎖被撤銷或遇到輕度競爭時,鎖會升級為輕量級鎖。其核心原理是通過CAS操作在棧幀中創建"鎖記錄"(Lock Record),并將對象頭的Mark Word替換為指向鎖記錄的指針:
-
線程在棧中創建Lock Record,復制對象頭Mark Word到Lock Record(Displaced Mark Word)
-
嘗試用CAS將對象頭Mark Word替換為指向Lock Record的指針
-
CAS成功則獲取鎖,失敗則進入自旋等待
-
自旋一定次數后仍未獲取鎖,則升級為重量級鎖
自旋優化的權衡
自旋等待(Spin Waiting)是指線程不放棄CPU,而是循環檢查鎖是否可用。這種方式避免了線程阻塞的開銷,但會消耗CPU資源。JVM通過-XX:PreBlockSpin
參數控制自旋次數,默認值為10次。在多核CPU環境下,自旋優化能顯著提升輕度競爭場景的性能。
輕量級鎖與偏向鎖的對比
特性 | 偏向鎖 | 輕量級鎖 |
---|---|---|
競爭程度 | 無競爭 | 輕度競爭 |
加鎖方式 | CAS記錄線程ID | CAS修改對象頭指針 |
解鎖開銷 | 幾乎無 | 需CAS還原Mark Word |
典型場景 | 單線程反復訪問 | 多線程交替訪問 |
重量級鎖:操作系統級別的同步
重量級鎖的底層實現
當輕量級鎖自旋超過閾值或競爭更加激烈時,鎖會膨脹為重量級鎖。此時JVM會調用操作系統的互斥量(Mutex)來實現線程阻塞,具體過程包括:
-
創建與對象關聯的監視器(Monitor)對象
-
線程進入監視器的等待隊列,狀態變為BLOCKED
-
釋放CPU資源,等待操作系統調度喚醒
-
喚醒后重新嘗試獲取鎖
重量級鎖的性能開銷
重量級鎖的性能開銷主要來自:
-
線程狀態切換(用戶態→內核態→用戶態)
-
操作系統調度器的上下文切換
-
等待隊列的管理開銷
在JDK6之前,Synchronized默認使用重量級鎖,這也是其"性能不佳"印象的來源。但經過鎖升級優化后,重量級鎖的使用場景已大幅減少。
鎖膨脹的觸發條件
鎖狀態從低到高升級的關鍵觸發條件包括:
-
偏向鎖遇到其他線程競爭
-
輕量級鎖自旋次數超過閾值(默認10次)
-
調用
Object.wait()
等會導致線程阻塞的方法 -
鎖競爭持續時間超過自旋優化的收益臨界點
Synchronized與Java內存模型(JMM)的深層聯系
JMM的核心架構
Java內存模型定義了線程和主內存之間的抽象關系:
-
主內存:所有線程共享的內存區域,存儲共享變量
-
工作內存:每個線程私有的內存區域,存儲共享變量的副本
這種架構導致了一個核心問題:線程間如何保證共享變量的可見性?Synchronized通過以下機制解決這一問題:
Synchronized的內存語義
當線程執行synchronized
同步塊時,會遵循以下內存規則:
-
進入同步塊:
-
從主內存讀取共享變量的最新值到工作內存
-
清空工作內存中與同步塊相關的變量副本
-
保證同步塊內操作的有序性(禁止指令重排序)
-
-
退出同步塊:
-
將工作內存中的變量修改刷新到主內存
-
確保所有對共享變量的修改對其他線程可見
-
建立happens-before關系,保證后續線程能看到最新數據
-
這種機制通過JVM在編譯時生成的monitorenter
和monitorexit
指令實現,確保了同步操作的可見性和有序性。
happens-before原則與Synchronized
JMM中的happens-before原則定義了操作之間的偏序關系,其中與Synchronized相關的規則包括:
-
監視器鎖規則:對一個鎖的解鎖操作happens-before于后續對該鎖的加鎖操作
-
程序順序規則:同步塊內的操作按程序順序執行
-
傳遞性:若A happens-before B且B happens-before C,則A happens-before C
這些規則共同保證了Synchronized同步塊內操作的正確性,例如:
private int x = 0;
private Object lock = new Object();
?
public void update() {synchronized (lock) {x = 1; // 操作1x = 2; // 操作2} // 解鎖操作,happens-before后續加鎖操作
}
?
public void read() {synchronized (lock) { // 加鎖操作,happens-after解鎖操作assert x == 2; // 一定成立}
}
由于解鎖操作happens-before加鎖操作,讀操作必然能看到寫操作的最新結果。
其他同步解決方案:與Synchronized的對比與互補
ReentrantLock:靈活的顯式鎖
ReentrantLock的核心特性
ReentrantLock(可重入鎖)是JUC包中提供的同步工具,與Synchronized相比具有以下優勢:
-
顯式鎖控制:通過
lock()
和unlock()
方法顯式獲取和釋放鎖 -
可中斷獲取鎖:支持
lockInterruptibly()
方法,可響應中斷 -
公平鎖機制:支持公平鎖模式,保證線程獲取鎖的順序
-
條件變量:通過
newCondition()
方法創建條件變量,實現更靈活的等待/通知機制
公平鎖與非公平鎖的實現差異
ReentrantLock支持兩種鎖模式:
-
非公平鎖(默認):新線程可能在等待隊列頭部線程之前獲取鎖,性能更高
-
公平鎖:嚴格按照線程等待順序獲取鎖,避免饑餓
// 創建公平鎖
ReentrantLock fairLock = new ReentrantLock(true);
?
// 使用示例
fairLock.lock();
try {// 同步代碼塊
} finally {fairLock.unlock();
}
公平鎖通過AQS
(AbstractQueuedSynchronizer)的等待隊列實現,而非公平鎖在獲取鎖時會先嘗試直接獲取,可能跳過等待隊列中的線程。
ReentrantLock與Synchronized的對比
特性 | Synchronized | ReentrantLock |
---|---|---|
鎖獲取方式 | 隱式(自動加鎖/解鎖) | 顯式(手動調用方法) |
可重入性 | 支持 | 支持 |
公平性 | 非公平 | 可選擇公平/非公平 |
鎖中斷 | 不支持 | 支持 |
條件變量 | 不支持 | 支持 |
性能(無競爭) | 優(偏向鎖優化) | 略遜 |
性能(高競爭) | 略遜 | 優(可中斷、公平鎖) |
ReadWriteLock:讀寫分離的同步策略
讀寫鎖的核心思想
ReadWriteLock(讀寫鎖)將鎖分為讀鎖和寫鎖,允許多個線程同時獲取讀鎖,但同一時刻只能有一個線程獲取寫鎖。這種設計特別適合"讀多寫少"的場景,例如緩存系統:
public class Cache {private final ReadWriteLock lock = new ReentrantReadWriteLock();private Map<String, Object> data = new HashMap<>();// 讀操作獲取讀鎖public Object get(String key) {lock.readLock().lock();try {return data.get(key);} finally {lock.readLock().unlock();}}// 寫操作獲取寫鎖public void put(String key, Object value) {lock.writeLock().lock();try {data.put(key, value);} finally {lock.writeLock().unlock();}}
}
讀寫鎖的狀態管理
ReentrantReadWriteLock通過一個整數(32位)來管理兩種鎖狀態:
-
高16位:記錄讀鎖的獲取次數(可被多個線程共享)
-
低16位:記錄寫鎖的獲取次數(僅能被一個線程持有)
這種設計使得讀寫鎖能在一個變量中維護兩種鎖狀態,提高了空間效率。
讀寫鎖的適用場景
讀寫鎖適合以下場景:
-
讀取操作頻率遠高于寫入操作
-
寫入操作耗時較短
-
需要保證讀取操作的一致性
例如:
-
配置文件讀取(很少修改,頻繁讀取)
-
緩存系統(讀多寫少)
-
數據庫查詢緩存(查詢頻繁,更新較少)
但需注意,讀寫鎖在寫操作頻繁的場景下性能可能不如普通互斥鎖,因為讀鎖的釋放可能導致寫鎖饑餓。
Synchronized在JDK源碼中的典型應用
容器類中的同步實現
StringBuffer的同步實現
StringBuffer是JDK中典型的線程安全容器,其所有關鍵方法都使用Synchronized修飾:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {// 構造函數public StringBuffer() {super(16);}// 同步追加方法public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}// 同步插入方法public synchronized StringBuffer insert(int offset, char str[]) {toStringCache = null;super.insert(offset, str);return this;}// 其他同步方法...
}
這種實現方式保證了StringBuffer在多線程環境下的安全性,但也意味著所有操作都需要獲取鎖,在高并發場景下可能成為性能瓶頸。
Vector的同步機制
Vector與ArrayList功能相似,但所有操作都是同步的:
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// 同步添加元素public synchronized boolean add(E e) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = e;return true;}// 同步獲取元素public synchronized E get(int index) {if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);return elementData(index);}// 其他同步方法...
}
與StringBuffer類似,Vector的同步實現保證了線程安全,但在并發環境下性能不如非同步容器。JDK推薦在非必要時使用ArrayList,僅在需要線程安全時通過Collections.synchronizedList(new ArrayList<>())
包裝。
基礎類庫中的同步應用
Hashtable的同步實現
Hashtable是Java早期的線程安全哈希表,其實現方式與Vector類似:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {// 同步put方法public synchronized V put(K key, V value) {// 檢查key是否為nullif (key == null) {throw new NullPointerException();}V oldValue = get(key);putVal(key, value, false);return oldValue;}// 同步get方法public synchronized V get(Object key) {if (key == null) {throw new NullPointerException();}Entry<?,?> e = getEntry(key);return (e == null) ? null : (V)e.value;}// 其他同步方法...
}
由于Hashtable的同步粒度較大(整個哈希表),在高并發場景下性能較差,因此JDK后來提供了ConcurrentHashMap
作為替代方案,其采用分段鎖機制大幅提升了并發性能。
自定義同步工具的基礎
Synchronized也是JDK中許多自定義同步工具的實現基礎,例如:
public class Semaphore {// 內部通過AQS實現,但AQS的底層操作依賴于CAS和Monitorpublic Semaphore(int permits) {sync = new NonfairSync(permits);}// 其他方法...
}
雖然Semaphore的上層接口不直接使用Synchronized,但底層AQS的實現仍然依賴于JVM的監視器鎖機制,體現了Synchronized在JDK中的基礎地位。
Synchronized的最佳實踐與性能優化
精細化同步范圍
最小化同步代碼塊
// 反例:同步范圍過大
public void badPractice() {synchronized (this) {// 非共享資源操作,無需同步loadConfig();// 共享資源操作,需要同步updateSharedData();// 非共享資源操作,無需同步logOperation();}
}// 正例:縮小同步范圍
public void goodPractice() {// 非共享資源操作loadConfig();// 僅同步必要的代碼塊synchronized (this) {updateSharedData();}// 非共享資源操作logOperation();
}
通過縮小同步范圍,可以減少線程競爭,提高并發性。基本原則是:只對真正訪問共享資源的代碼加鎖。
同步對象的選擇
優先使用私有鎖對象:
private final Object lock = new Object();public void operation() {synchronized (lock) {// 同步代碼}
}
私有鎖對象避免了外部代碼直接訪問鎖,減少了鎖競爭的意外風險。
避免使用this作為鎖對象:
public void badLock() {synchronized (this) { // 危險!外部可能獲取this鎖導致死鎖// 同步代碼}
}
除非類本身設計為線程安全的同步組件,否則應避免使用this作為鎖對象。
利用鎖的可重入性
可重入性的實際應用
public class ReentrantDemo {public synchronized void method1() {System.out.println("進入method1");method2(); // 調用同步方法method2System.out.println("退出method1");}public synchronized void method2() {System.out.println("進入method2");// 其他操作System.out.println("退出method2");}
}
在上述示例中,method1
調用method2
時,由于Synchronized鎖的可重入性,線程無需再次獲取鎖,避免了死鎖風險。可重入性是通過鎖的計數器實現的,每次進入同步塊時計數器加1,退出時減1,當計數器為0時才真正釋放鎖。
可重入性與繼承場景
class Parent {public synchronized void operation() {System.out.println("Parent operation");}
}class Child extends Parent {@Overridepublic synchronized void operation() {System.out.println("Child before");super.operation(); // 調用父類同步方法System.out.println("Child after");}
}
在繼承場景下,子類重寫同步方法并調用父類方法時,可重入性保證了鎖的正確獲取,避免了因多次加鎖導致的死鎖。
性能優化參數調整
偏向鎖相關參數
啟用/禁用偏向鎖:
-XX:+UseBiasedLocking // 啟用偏向鎖(JDK6后默認啟用) -XX:-UseBiasedLocking // 禁用偏向鎖
偏向鎖延遲激活:
-XX:BiasedLockingStartupDelay=0 // 啟動時立即激活偏向鎖(默認延遲4秒)
輕量級鎖自旋參數
自旋次數設置:
-XX:PreBlockSpin=20 // 設置自旋次數(默認10次)
自適應自旋:
-XX:+UseAdaptiveSpinning // 啟用自適應自旋(JDK6后默認啟用)
自適應自旋會根據前一次自旋的成功情況動態調整自旋次數,提高優化效果。
場景化選擇策略
選擇Synchronized的場景
-
簡單同步需求:無需復雜鎖控制的場景
-
單線程或低競爭環境:偏向鎖和輕量級鎖能發揮最佳性能
-
代碼簡潔性優先:隱式加鎖/解鎖減少代碼量
-
與JMM結合的場景:需要利用Synchronized的內存語義保證可見性
選擇ReentrantLock的場景
-
需要公平鎖機制:避免線程饑餓
-
需要可中斷鎖:響應線程中斷
-
需要條件變量:實現更靈活的等待/通知機制
-
高競爭環境:ReentrantLock的性能可能更優
-
需要手動控制鎖釋放:如配合
try-finally
確保解鎖
選擇ReadWriteLock的場景
-
讀多寫少的場景:如緩存、配置文件等
-
需要讀寫分離:提高讀操作的并發性
-
寫入操作耗時較短:避免讀鎖饑餓
總結
從"重量級鎖"到"智能鎖"的進化
回顧Synchronized的發展歷程,我們可以看到JVM團隊在性能優化上的持續努力:
-
JDK1.0-1.5:僅支持重量級鎖,性能較差
-
JDK6:引入偏向鎖、輕量級鎖,大幅提升性能
-
JDK7:優化鎖膨脹路徑,減少重量級鎖的使用
-
JDK8+:進一步優化偏向鎖的獲取和撤銷流程
這種進化使得Synchronized在無競爭和輕度競爭場景下的性能接近無鎖操作,重新成為Java并發編程的首選同步工具之一。
同步機制的選擇原則
在實際開發中,選擇同步機制應遵循以下原則:
-
優先使用Synchronized:對于大多數場景,Synchronized已足夠高效,且代碼更簡潔
-
ReentrantLock作為補充:當需要公平鎖、可中斷鎖或條件變量時使用
-
ReadWriteLock謹慎使用:僅在讀多寫少場景下使用,避免寫鎖饑餓
-
性能測試驗證:不同場景下鎖的性能表現可能不同,需通過實測確定最優方案
核心知識回顧
-
鎖狀態體系:無鎖→偏向鎖→輕量級鎖→重量級鎖的狀態轉換
-
實現原理:基于JVM監視器鎖,通過Mark Word記錄鎖狀態
-
內存語義:保證共享變量的可見性和操作的有序性
-
性能優化:偏向鎖、輕量級鎖、自旋等待等優化手段
-
應用場景:結合具體業務需求選擇合適的同步方案
掌握Synchronized的原理與應用,是成為Java并發編程高手的必經之路。通過深入理解其底層實現和優化機制,我們能夠更精準地運用這一強大工具,構建高效、安全的多線程應用。