在前幾篇文章中,我們探討了無鎖同步機制,這固然是解決多線程問題的理想方案。然而在實際開發中,許多場景仍不得不依賴鎖同步來保證線程安全。自 JDK 5 起,Java 提供了兩種並行訪問控制機制:synchronized 關鍵字與 ReentrantLock。既然 synchronized 已經介紹過,本文將重點分析 ReentrantLock 如何改善傳統鎖的不足。
synchronized 的兩大痛點實際使用中,synchronized 主要存在以下限制:
wait()/notify()),大量併發線程的等待與喚醒管理極其困難。以下代碼演示了 ReentrantLock 如何通過 lockInterruptibly() 響應線程中斷:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockSample {
public static void main(String[] args) {
testSynchronized();
testReentrantLock();
}
public static void testReentrantLock() {
final SampleSupport1 support = new SampleSupport1();
Thread first = new Thread(() -> {
try { support.doSomething(); }
catch (InterruptedException e) { e.printStackTrace(); }
});
Thread second = new Thread(() -> {
try { support.doSomething(); }
catch (InterruptedException e) {
System.out.println("Second Thread Interrupted without executing counter++");
}
});
executeTest(first, second);
}
public static void testSynchronized() {
final SampleSupport2 support2 = new SampleSupport2();
Runnable runnable = () -> support2.doSomething();
Thread third = new Thread(runnable);
Thread fourth = new Thread(runnable);
executeTest(third, fourth);
}
public static void executeTest(Thread a, Thread b) {
a.start();
try {
Thread.sleep(100);
b.start();
Thread.sleep(1000);
b.interrupt(); // 1秒後嘗試中斷第二個線程
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
abstract class SampleSupport {
protected int counter;
public void startTheCountdown() {
long currentTime = System.currentTimeMillis();
while (System.currentTimeMillis() - currentTime <= 5000) { /* busy wait */ }
}
}
class SampleSupport1 extends SampleSupport {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() throws InterruptedException {
lock.lockInterruptibly(); // 關鍵點:可中斷獲取鎖
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
try { counter++; }
finally { lock.unlock(); }
}
}
class SampleSupport2 extends SampleSupport {
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " will execute counter++.");
startTheCountdown();
counter++;
}
}
運行結果分析:
synchronized 版本:第二個線程即使被 interrupt() 仍會繼續等待約 5 秒獲取鎖並執行 counter++。ReentrantLock 版本:第二個線程在等待鎖期間收到中斷信號,拋出 InterruptedException 並退出競爭,不再執行 counter++。ReentrantLock 另一大優勢是支持多個 Condition 對象,實現精細化的線程協調。以 ArrayBlockingQueue 為例:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> {
private final ReentrantLock lock;
private final Condition notEmpty; // 等待取出的線程
private final Condition notFull; // 等待放入的線程
public ArrayBlockingQueue(int capacity, boolean fair) {
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == items.length) notFull.await(); // 隊列滿,等待 notFull
insert(e);
notEmpty.signal(); // 喚醒等待取出的線程
} finally { lock.unlock(); }
}
public E take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == 0) notEmpty.await(); // 隊列空,等待 notEmpty
return extract();
} finally { lock.unlock(); }
}
// ... insert/extract 省略
}
這裡 notEmpty 和 notFull 分別管理「消費者」與「生產者」線程,避免了 synchronized 單一條件導致的「驚群效應」與管理混亂。
synchronized 相當(JDK 6+ 優化後)。finally 塊中顯式調用 unlock(),否則易發生死鎖;代碼較繁瑣。synchronized(語法簡潔、JVM 自動釋放鎖、逃逸分析優化更好)。下一篇將介紹 ReentrantReadWriteLock,實現讀寫分離的部分鎖機制。