Java中的鎖機制
在Java中,鎖機制是多線程編程里保障數據一致性與線程安全的關鍵技術。
1. 內置鎖:synchronized關鍵字
synchronized
是Java的內置鎖機制,能夠保證在同一時刻,只有一個線程可以執行被其修飾的代碼塊或方法。
用法示例
public class SynchronizedExample {private int count = 0;// 同步方法public synchronized void increment() {count++;}// 同步代碼塊public void decrement() {synchronized(this) {count--;}}
}
實現原理
synchronized
是基于對象頭中的Mark Word來實現的。當一個線程訪問同步代碼塊時,會先查看對象的Mark Word。如果Mark Word顯示該對象沒有被鎖定,那么這個線程就會將Mark Word設置為鎖定狀態,然后開始執行同步代碼塊。在這個線程執行同步代碼塊期間,如果其他線程也想訪問這個同步代碼塊,它們會發現對象的Mark Word已經被設置為鎖定狀態,于是這些線程就會被阻塞,進入等待隊列。
2. 顯示鎖:Lock接口
Lock
接口是Java 5引入的,它提供了比synchronized
更靈活、更強大的鎖控制能力。
核心方法
lock()
:獲取鎖,如果鎖不可用,則線程會被阻塞。unlock()
:釋放鎖,必須在finally
塊中調用,以確保鎖一定會被釋放。tryLock()
:嘗試獲取鎖,如果鎖可用,則獲取鎖并返回true
;如果鎖不可用,則立即返回false
,不會阻塞線程。
用法示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
3. ReentrantLock(可重入鎖)
ReentrantLock
是Lock
接口的一個重要實現類,它支持可重入鎖的特性。所謂可重入鎖,就是指同一個線程可以多次獲取同一把鎖,而不會出現死鎖的情況。
特性
- 公平性:
ReentrantLock
可以設置為公平鎖或非公平鎖。公平鎖會按照線程請求鎖的順序來分配鎖,而非公平鎖則不保證這一點,有可能后請求的線程先獲得鎖。 - 可重入性:同一個線程可以多次獲取同一把鎖,每獲取一次,鎖的計數器就會加1,每釋放一次,鎖的計數器就會減1,當計數器為0時,鎖才會被真正釋放。
公平鎖示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final Lock fairLock = new ReentrantLock(true); // true表示公平鎖public void performTask() {fairLock.lock();try {// 執行任務} finally {fairLock.unlock();}}
}
4. ReentrantReadWriteLock(讀寫鎖)
ReentrantReadWriteLock
提供了讀寫分離的鎖機制,它維護了一對鎖,一個讀鎖和一個寫鎖。
特性
- 讀鎖:允許多個線程同時獲取讀鎖,用于并發讀取共享資源。
- 寫鎖:寫鎖是排他鎖,同一時刻只允許一個線程獲取寫鎖,用于修改共享資源。
- 讀寫互斥:讀鎖和寫鎖不能同時被獲取,即當有線程獲取了讀鎖時,其他線程不能獲取寫鎖;當有線程獲取了寫鎖時,其他線程不能獲取讀鎖和寫鎖。
用法示例
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {private final ReadWriteLock rwLock = new ReentrantReadWriteLock();private Object data;public Object read() {rwLock.readLock().lock();try {return data;} finally {rwLock.readLock().unlock();}}public void write(Object newData) {rwLock.writeLock().lock();try {data = newData;} finally {rwLock.writeLock().unlock();}}
}
5. StampedLock(郵戳鎖)
StampedLock
是Java 8引入的一種新的鎖機制,它提供了比ReentrantReadWriteLock
更細粒度的鎖控制。可以有效應對A-B-A問題。
特性
- 樂觀讀鎖:樂觀讀鎖是一種無鎖機制,它允許在沒有獲取鎖的情況下讀取共享資源。讀取完成后,需要驗證資源是否在讀取期間被修改過,如果沒有被修改過,則讀取有效;如果被修改過,則需要重新讀取。
- 悲觀讀鎖和寫鎖:與
ReentrantReadWriteLock
的讀鎖和寫鎖類似,但StampedLock
的悲觀讀鎖和寫鎖是通過返回一個郵戳(stamp)來控制的。
用法示例
import java.util.concurrent.locks.StampedLock;public class Point {private double x, y;private final StampedLock sl = new StampedLock();public double distanceFromOrigin() {long stamp = sl.tryOptimisticRead(); // 嘗試樂觀讀double currentX = x, currentY = y;if (!sl.validate(stamp)) { // 驗證是否有寫操作發生stamp = sl.readLock(); // 獲取悲觀讀鎖try {currentX = x;currentY = y;} finally {sl.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}
}
6. 鎖的優化
鎖粗化(Lock Coarsening)
鎖粗化是指將多次連續的加鎖和解鎖操作合并為一次加鎖和解鎖操作,以減少鎖的獲取和釋放帶來的性能開銷。
鎖消除(Lock Elimination)
鎖消除是指在編譯時,Java編譯器會對一些代碼進行分析,如果發現某些鎖是不必要的,就會將這些鎖消除掉,從而提高代碼的執行效率。
偏向鎖(Biased Locking)
偏向鎖是一種針對單線程環境的鎖優化機制。當一個線程第一次獲取鎖時,鎖會被標記為偏向鎖,并記錄該線程的ID。當該線程再次獲取鎖時,無需進行任何同步操作,直接獲取鎖,從而提高了單線程環境下的性能。
輕量級鎖(Lightweight Locking)
輕量級鎖是一種在多線程環境下,但線程競爭不激烈時的鎖優化機制。當線程競爭不激烈時,輕量級鎖可以避免線程的阻塞和喚醒操作,從而提高了性能。
自旋鎖(Spin Lock)
自旋鎖是指當一個線程獲取鎖失敗時,它不會立即被阻塞,而是會在原地循環等待,直到鎖被釋放。自旋鎖適用于鎖的持有時間較短的場景,可以減少線程的阻塞和喚醒帶來的性能開銷。
7. 選擇合適的鎖
- synchronized:適用于簡單的同步場景,代碼簡潔,由JVM自動管理鎖的獲取和釋放。
- ReentrantLock:適用于需要更靈活的鎖控制的場景,如可中斷鎖、公平鎖等。
- ReentrantReadWriteLock:適用于讀多寫少的場景,可以提高并發讀取的性能。
- StampedLock:適用于讀多寫少且讀操作性能要求較高的場景,提供了樂觀讀鎖機制,進一步提高了讀操作的性能。
通過合理使用這些鎖機制,你可以在多線程編程中實現高效且安全的并發控制。
重入鎖和內置鎖的選用
在Java里,重入鎖(ReentrantLock)和內置鎖(synchronized)都可用于實現線程同步,不過它們的適用場景存在差異。下面為你介紹選擇使用重入鎖還是內置鎖的依據:
優先考慮內置鎖的情況
- 語法簡潔:內置鎖是通過
synchronized
關鍵字來實現的,無需手動釋放鎖,JVM會自動處理鎖的獲取和釋放,這樣能降低因忘記釋放鎖而導致死鎖的風險。public synchronized void method() {// 同步代碼塊 }
- 對性能要求不高:在JDK 1.6之后,Java對
synchronized
進行了一系列優化,像偏向鎖、輕量級鎖等,使得它的性能和ReentrantLock
相差不大。 - 鎖的使用場景簡單:若只是需要對方法或代碼塊進行簡單的同步,內置鎖完全可以滿足需求。
優先考慮重入鎖的情況
- 需要公平鎖:重入鎖可以通過構造函數指定使用公平鎖(
new ReentrantLock(true)
),公平鎖會按照線程請求鎖的順序來分配鎖,能避免某些線程長時間等待鎖的情況。而內置鎖只能是非公平鎖。 - 需要靈活的鎖控制:重入鎖提供了一些高級功能,如可中斷鎖、嘗試鎖(
tryLock()
)、帶超時的鎖獲取等。Lock lock = new ReentrantLock(); try {// 嘗試獲取鎖,若鎖被其他線程持有,則當前線程可被中斷lock.lockInterruptibly();// 執行同步操作 } catch (InterruptedException e) {Thread.currentThread().interrupt(); } finally {lock.unlock(); }
- 需要實現條件變量(Condition):重入鎖可以和
Condition
接口配合使用,實現更靈活的線程等待和喚醒機制,比如實現生產者 - 消費者模式。Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();// 等待條件 lock.lock(); try {while (!conditionMet()) {condition.await();} } finally {lock.unlock(); }// 喚醒等待的線程 lock.lock(); try {condition.signalAll(); } finally {lock.unlock(); }
性能方面的考量
- 低競爭場景:在競爭不激烈的情況下,內置鎖和重入鎖的性能差距不大。
- 高競爭場景:如果線程之間對鎖的競爭非常激烈,重入鎖的性能可能會略優于內置鎖,因為重入鎖提供了更多的鎖優化選項,例如使用非公平鎖可以減少線程的上下文切換。
總結
- 推薦優先使用內置鎖:因為它的語法簡潔,由JVM自動管理鎖的獲取和釋放,降低了出錯的概率。
- 在需要高級特性時使用重入鎖:例如公平鎖、可中斷鎖、條件變量等。
示例對比
內置鎖示例
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}
}
重入鎖示例
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
通過對比可以看出,內置鎖的代碼更加簡潔,而重入鎖則提供了更靈活的控制方式。你可以根據具體的需求來選擇合適的鎖機制。
理解各種鎖的特性和適用場景
Java鎖機制中的各類鎖是根據不同場景和需求設計的,理解它們的核心概念、實現方式及適用場景對編寫高效、線程安全的代碼至關重要。以下從分類維度詳細解析常見的鎖類型:
一、按鎖的特性分類
1. 樂觀鎖(Optimistic Locking)
- 核心思想:假設數據不會被其他線程修改,因此不阻塞訪問,僅在更新時檢查是否有沖突。
- 實現方式:
- 版本號機制:數據記錄中增加
version
字段,更新時比較版本號是否一致(如數據庫的UPDATE ... WHERE version = ?
); - CAS(Compare-And-Swap):Java中通過
Unsafe
類或Atomic
包實現(如AtomicInteger
的getAndIncrement()
)。
- 版本號機制:數據記錄中增加
- 使用場景:讀多寫少、沖突概率低的場景(如緩存更新、統計計數)。
- 示例:
// AtomicInteger基于CAS實現樂觀鎖 private AtomicInteger counter = new AtomicInteger(0); public void increment() {counter.getAndIncrement(); // 內部使用CAS,無需顯式加鎖 }
2. 悲觀鎖(Pessimistic Locking)
- 核心思想:假設數據隨時會被修改,因此訪問時直接加鎖,阻塞其他線程。
- 實現方式:
- synchronized關鍵字:Java內置的對象鎖(如
synchronized(this)
); - ReentrantLock:可重入鎖,功能更靈活(如支持公平鎖、可中斷鎖)。
- synchronized關鍵字:Java內置的對象鎖(如
- 使用場景:寫多、沖突概率高的場景(如庫存扣減、銀行轉賬)。
- 示例:
// synchronized實現悲觀鎖 public synchronized void transferMoney() {// 操作共享資源 }
3. 自旋鎖(Spin Lock)
- 核心思想:當鎖被占用時,線程不掛起(不放棄CPU時間片),而是循環嘗試獲取鎖,直到成功。
- 實現方式:
- 基于CAS實現(如
AtomicBoolean
的compareAndSet()
); - JVM內部的輕量級鎖(偏向鎖膨脹后可能升級為自旋鎖)。
- 基于CAS實現(如
- 使用場景:鎖持有時間短、線程不希望頻繁掛起/喚醒的場景(如JDK內部的
ConcurrentHashMap
)。 - 優點:避免線程上下文切換,提升性能;
- 缺點:若鎖長時間被占用,會浪費CPU資源。
- 示例:
// 自定義簡單自旋鎖 public class SpinLock {private AtomicBoolean locked = new AtomicBoolean(false);public void lock() {while (!locked.compareAndSet(false, true)) {// 循環等待,自旋}}public void unlock() {locked.set(false);} }
二、按JVM實現分類
4. 偏向鎖(Biased Locking)
- 核心思想:在單線程環境下,鎖偏向第一個獲取它的線程,后續該線程無需再進行同步操作。
- 實現方式:
- 對象頭中的
Mark Word
存儲偏向線程ID; - 當有其他線程嘗試競爭鎖時,偏向鎖會被撤銷并升級為輕量級鎖。
- 對象頭中的
- 使用場景:只有一個線程訪問同步塊的場景(如單例模式的雙重檢查鎖)。
- 優點:無競爭時幾乎無開銷,提升單線程性能。
5. 輕量級鎖(Lightweight Lock)
- 核心思想:多線程交替執行同步塊時,通過CAS避免重量級鎖的線程掛起/喚醒操作。
- 實現方式:
- 線程進入同步塊時,JVM在棧幀中創建鎖記錄(Lock Record);
- 通過CAS將對象頭的
Mark Word
指向鎖記錄,成功則獲取鎖,失敗則升級為重量級鎖。
- 使用場景:線程競爭不激烈,鎖持有時間短的場景。
6. 重量級鎖(Heavyweight Lock)
- 核心思想:依賴操作系統的互斥量(Mutex)實現,線程競爭鎖失敗時會被掛起(進入內核態)。
- 實現方式:
- 基于操作系統的
pthread_mutex_t
(Linux)或CRITICAL_SECTION
(Windows); - 對象頭的
Mark Word
指向重量級鎖的指針,鎖競爭時涉及用戶態與內核態的切換。
- 基于操作系統的
- 使用場景:線程競爭激烈,鎖持有時間長的場景。
- 缺點:性能開銷大,因為涉及內核態與用戶態的切換。
三、按API實現分類
7. 可重入鎖(Reentrant Lock)
- 核心思想:允許同一個線程多次獲取同一把鎖,而不會被阻塞(通過計數器實現)。
- 實現方式:
synchronized
關鍵字(隱式可重入);ReentrantLock
(顯式可重入,需調用lock()
和unlock()
)。
- 使用場景:方法嵌套調用同步塊的場景(如遞歸函數)。
- 示例:
// ReentrantLock實現可重入鎖 private ReentrantLock lock = new ReentrantLock(); public void outerMethod() {lock.lock();try {innerMethod();} finally {lock.unlock();} }public void innerMethod() {lock.lock(); // 同一線程可再次獲取鎖try {// 操作共享資源} finally {lock.unlock();} }
8. 公平鎖(Fair Lock)
- 核心思想:鎖的獲取順序按請求時間排序,先到先得(避免線程“饑餓”)。
- 實現方式:
ReentrantLock(true)
:構造函數傳入true
啟用公平鎖;- 基于FIFO隊列實現,線程競爭鎖時會進入隊列等待。
- 使用場景:對線程執行順序敏感的場景(如資源分配)。
- 缺點:公平鎖的性能通常低于非公平鎖,因為需要維護隊列。
9. 讀寫鎖(ReadWrite Lock)
- 核心思想:將鎖分為讀鎖和寫鎖,允許多個線程同時讀,但寫時互斥。
- 實現方式:
ReentrantReadWriteLock
:支持讀鎖(共享鎖)和寫鎖(排他鎖);- 讀鎖可被多個線程同時持有,寫鎖只能被一個線程持有,且寫鎖存在時禁止讀鎖。
- 使用場景:讀多寫少的場景(如緩存更新、配置讀取)。
- 示例:
// 讀寫鎖示例 private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private Lock readLock = rwLock.readLock(); private Lock writeLock = rwLock.writeLock();public void readData() {readLock.lock();try {// 讀取共享數據} finally {readLock.unlock();} }public void writeData() {writeLock.lock();try {// 修改共享數據} finally {writeLock.unlock();} }
10. 分段鎖(Striped Lock)
- 核心思想:將鎖分段管理,不同段的鎖相互獨立,減少鎖競爭。
- 實現方式:
ConcurrentHashMap
(JDK 7及以前):內部使用分段數組(Segment),每個Segment獨立加鎖;- JDK 8后改用CAS+Synchronized實現,但分段鎖思想仍在其他場景中使用。
- 使用場景:大規模數據并發操作的場景(如分布式緩存)。
四、按鎖的特性擴展
11. 可中斷鎖(Interruptible Lock)
- 核心思想:線程在等待鎖的過程中可被其他線程中斷。
- 實現方式:
ReentrantLock
的lockInterruptibly()
方法;synchronized
不支持可中斷,只能通過Thread.interrupt()
標記中斷狀態。
- 使用場景:需要取消長時間等待的場景(如超時控制)。
- 示例:
ReentrantLock lock = new ReentrantLock(); try {lock.lockInterruptibly(); // 可中斷的鎖獲取try {// 操作共享資源} finally {lock.unlock();} } catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢復中斷狀態 }
12. 鎖降級(Lock Degradation)
- 核心思想:將寫鎖降級為讀鎖,保持數據可見性。
- 實現方式:
- 先獲取寫鎖,修改數據后獲取讀鎖,再釋放寫鎖。
- 使用場景:寫操作后需要保證后續讀操作可見性的場景(如緩存更新)。
- 示例:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock writeLock = rwLock.writeLock(); Lock readLock = rwLock.readLock();public void updateAndRead() {writeLock.lock();try {// 修改數據readLock.lock(); // 獲取讀鎖} finally {writeLock.unlock(); // 釋放寫鎖,保留讀鎖(鎖降級)}try {// 讀取數據(確保數據可見性)} finally {readLock.unlock();} }
五、鎖的選擇策略
- 優先考慮無鎖方案:如使用
Atomic
類、ConcurrentHashMap
等無鎖數據結構。 - 讀多寫少→樂觀鎖/CAS:沖突概率低時性能最優。
- 寫多沖突高→悲觀鎖:如
synchronized
或ReentrantLock
。 - 鎖持有時間短→自旋鎖:避免線程上下文切換。
- 公平性需求→公平鎖:但需犧牲一定性能。
- 讀寫分離→讀寫鎖:如
ReentrantReadWriteLock
。
理解各種鎖的特性和適用場景,結合具體業務需求選擇合適的鎖,是編寫高效并發代碼的關鍵。