我們今天來談Java中的鎖定機制和條件變數,包括:
我們先回顧一下Java中的基本同步概念。
synchronized 關鍵字:
範例:
public synchronized void synchronizedMethod() {
// 同步方法內容
}
public void synchronizedBlock() {
synchronized(this) {
// 同步區塊內容
}
}
volatile 關鍵字:
範例:
private volatile boolean flag = false;
等待與通知機制:
範例:
synchronized(sharedObject) {
while(!condition) {
sharedObject.wait();
}
// 執行操作
sharedObject.notifyAll();
}
執行緒安全的概念:
原子性、可見性和有序性:
這些基本概念構成了Java同步機制的基礎。然而,在複雜的並行場景中,這些基本工具可能顯得不夠靈活或效率不高。
這就是為什麼Java引入了更進階的鎖定機制和條件變數。
Java 5引入了java.util.concurrent.locks包,提供了比synchronized更靈活的鎖定機制,其中,Lock介面是這個包的核心,而ReentrantLock是Lock介面最常用的實現。
Lock介面:
Lock介面定義了鎖的基本操作,包括獲取鎖、釋放鎖、嘗試獲取鎖等。主要方法包括:
ReentrantLock:
ReentrantLock是Lock介面的可重入實現,它允許同一個執行緒多次獲取同一個鎖。
特點:
使用範例:
Lock lock = new ReentrantLock();
try {
lock.lock();
// 臨界區程式碼
} finally {
lock.unlock();
}
進階用法:
if (lock.tryLock()) {
try {
// 獲取到鎖後的操作
} finally {
lock.unlock();
}
} else {
// 未獲取到鎖的處理
}
try {
lock.lockInterruptibly();
// 臨界區程式碼
} catch (InterruptedException e) {
// 處理中斷
} finally {
lock.unlock();
}
Lock fairLock = new ReentrantLock(true);
ReentrantLock vs synchronized:
使用ReentrantLock時的注意事項:
ReentrantLock提供了比synchronized更高的靈活性和功能,但也需要更謹慎的使用。
在選擇使用ReentrantLock時,應該權衡其優勢和複雜性,確保正確使用以避免並發問題。
ReentrantReadWriteLock是Java並發包中提供的一種先進的鎖機制,允許多個讀操作同時進行,但寫操作則是互斥的。
這種機制在「讀多寫少」的場景中特別有用,可以顯著提高程式的並行性能。
ReentrantReadWriteLock的主要特性:
基本使用方式:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 讀操作
readLock.lock();
try {
// 讀取共享資源
} finally {
readLock.unlock();
}
// 寫操作
writeLock.lock();
try {
// 修改共享資源
} finally {
writeLock.unlock();
}
進階用法:
writeLock.lock();
try {
// 寫操作
readLock.lock();
writeLock.unlock();
// 現在持有讀鎖,可以繼續讀操作
} finally {
readLock.unlock();
}
Condition condition = writeLock.newCondition();
使用場景:
效能考量:
注意事項:
條件變數(Condition)是Java並發程式設計中的一個重要概念,讓執行緒可以在某個條件成立之前等待,並在條件滿足時被喚醒。Condition與Lock介面緊密相關,替代傳統的Object.wait()、Object.notify()和Object.notifyAll()方法,提供靈活的執行緒間協調機制。
主要特點:
基本使用方式:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待條件
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
// 條件滿足,執行相應操作
} finally {
lock.unlock();
}
// 通知條件可能滿足
lock.lock();
try {
// 改變條件狀態
condition.signal(); // 或 condition.signalAll();
} finally {
lock.unlock();
}
主要方法:
進階用法:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
if (condition.await(1, TimeUnit.SECONDS)) {
// 在超時前條件滿足
} else {
// 超時
}
使用場景:
與傳統wait/notify的比較:
注意事項:
StampedLock是Java 8引入的一個新的鎖機制,在某些場景下可以替代ReentrantReadWriteLock,提供更好的性能。
StampedLock的主要特點是它支援樂觀讀(optimistic reading),這在讀多寫少的場景中可以大幅提升性能。
主要特性:
基本使用方式:
StampedLock lock = new StampedLock();
long stamp = lock.writeLock();
try {
// 寫操作
} finally {
lock.unlockWrite(stamp);
}
long stamp = lock.readLock();
try {
// 讀操作
} finally {
lock.unlockRead(stamp);
}
long stamp = lock.tryOptimisticRead();
// 讀取共享變數
if (!lock.validate(stamp)) {
// 樂觀讀失敗,升級為讀鎖
stamp = lock.readLock();
try {
// 重新讀取共享變數
} finally {
lock.unlockRead(stamp);
}
}
進階用法:
long stamp = lock.readLock();
try {
// 讀操作
if (needWrite) {
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0L) {
stamp = writeStamp;
// 寫操作
} else {
// 轉換失敗,手動釋放讀鎖並獲取寫鎖
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
lock.unlock(stamp);
}
性能優勢:
使用注意事項:
適用場景:
在Java的鎖機制中,公平性是一個重要的概念,決定在競爭激烈的情況下,執行緒獲取鎖的順序。
公平鎖與非公平鎖的定義:
ReentrantLock中的公平性設置:
// 默認為非公平鎖
ReentrantLock unfairLock = new ReentrantLock();
// 創建公平鎖
ReentrantLock fairLock = new ReentrantLock(true);
公平鎖的優點:
非公平鎖的優點:
性能比較:
使用場景:
公平鎖:
非公平鎖:
注意事項:
選擇建議:
在並發程式設計中,死鎖、活鎖和飢餓是三種常見的問題,可能導致程式無法正常運行或效能嚴重下降。
死鎖的四個必要條件:
範例:
public void method1() {
synchronized(lockA) {
synchronized(lockB) {
// 操作
}
}
}
public void method2() {
synchronized(lockB) {
synchronized(lockA) {
// 操作
}
}
}
避免死鎖的方法:
範例:
while(true) {
if (tryLock(resource1)) {
if (tryLock(resource2)) {
// 使用資源
break;
} else {
unlock(resource1);
}
}
// 等待一段時間再重試
Thread.sleep(random.nextInt(1000));
}
避免活鎖的方法:
造成飢餓的原因:
避免飢餓的方法:
檢測並發問題的工具:
操作建議:
在使用Java的鎖定機制和條件變數時,遵循最佳實踐並考慮效能因素是非常重要的。
以下是一些關鍵的建議和考量:
- 正確響應 InterruptedException,不要忽視它
- 在長時間運行的操作中定期檢查中斷狀態
- 不要同步不可變對象
- 使用 volatile 關鍵字來保證可見性,而不是使用同步塊
- 使用 JMH(Java Microbenchmark Harness)進行微基準測試
- 利用 JVisualVM 或 Java Mission Control 進行性能分析
- 在適當的場景中,考慮使用 AtomicInteger、AtomicReference 等原子類
- 熟悉並適當使用 CAS(Compare-and-Swap)操作
- 詳細記錄同步策略和潛在的死鎖風險
- 進行定期的並發相關程式碼審查
本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI