首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 多線程讀寫鎖原理深度解析:從 ReentrantReadWriteLock 到自定義實現

     編輯:Java 相關知識

一、讀寫鎖核心語義

操作獲取條件並發性
讀鎖(共享鎖)無線程持有寫鎖,且無線程等待寫鎖多線程可同時持有
寫鎖(排他鎖)無線程持有讀鎖或寫鎖獨佔,阻塞其他讀/寫

寫優先策略:為防止寫線程飢餓,有寫請求時後續讀請求需等待。

二、簡易版 ReadWriteLock 實現(教學模型)


public class SimpleReadWriteLock {
    private int readingReaders = 0;      // 正在讀的線程數
    private int waitingWriters = 0;      // 等待寫的線程數
    private int writingWriter = 0;       // 正在寫的線程數 (0/1)
    
    public synchronized void lockRead() throws InterruptedException {
        while (writingWriter > 0 || waitingWriters > 0) {
            wait();  // 有寫在執行或等待,讀等待
        }
        readingReaders++;
    }
    
    public synchronized void unlockRead() {
        readingReaders--;
        notifyAll();  // 喚醒等待的寫線程
    }
    
    public synchronized void lockWrite() throws InterruptedException {
        waitingWriters++;
        while (readingReaders > 0 || writingWriter > 0) {
            wait();  // 有讀或寫在執行,寫等待
        }
        waitingWriters--;
        writingWriter = 1;
    }
    
    public synchronized void unlockWrite() {
        writingWriter = 0;
        notifyAll();  // 喚醒所有等待讀/寫線程
    }
}

關鍵點:notifyAll() 而非 notify(),避免信號丟失(喚醒的線程可能不符合條件需重新等待)。

三、可重入性:讀寫鎖的進階挑戰

問題場景

  1. 線程 A 持有讀鎖
  2. 線程 B 請求寫鎖 → 阻塞,waitingWriters++
  3. 線程 A 再次請求讀鎖 → 按簡易版邏輯被阻塞(waitingWriters > 0)
  4. 死鎖形成:A 等待 B 釋放,B 等待 A 釋放

解決方案:線程級計數器


public class ReentrantReadWriteLock {
    // 讀鎖:Thread -> 重入計數
    private final Map readingThreads = new HashMap<>();
    // 寫鎖:持有線程 + 重入計數
    private Thread writingThread = null;
    private int writeCount = 0;
    private int waitingWriters = 0;
    
    public synchronized void lockRead() throws InterruptedException {
        Thread t = Thread.currentThread();
        while (!canGrantRead(t)) wait();
        readingThreads.put(t, readingThreads.getOrDefault(t, 0) + 1);
    }
    
    private boolean canGrantRead(Thread t) {
        if (writingThread != null && writingThread != t) return false;  // 他人持有寫鎖
        if (readingThreads.containsKey(t)) return true;                 // 自己已持有讀鎖 -> 重入
        return waitingWriters == 0;                                     // 無寫等待 -> 允許新讀
    }
    
    public synchronized void unlockRead() {
        Thread t = Thread.currentThread();
        int count = readingThreads.getOrDefault(t, 0);
        if (count == 0) throw new IllegalMonitorStateException("無讀鎖");
        if (count == 1) readingThreads.remove(t);
        else readingThreads.put(t, count - 1);
        notifyAll();
    }
    
    public synchronized void lockWrite() throws InterruptedException {
        Thread t = Thread.currentThread();
        waitingWriters++;
        while (!canGrantWrite(t)) wait();
        waitingWriters--;
        writingThread = t;
        writeCount++;
    }
    
    private boolean canGrantWrite(Thread t) {
        if (!readingThreads.isEmpty() && !readingThreads.containsKey(t)) return false;  // 他人持有讀鎖
        if (writingThread == null) return true;                                         // 無寫鎖
        return writingThread == t;                                                      // 自己持有寫鎖 -> 重入
    }
    
    public synchronized void unlockWrite() {
        Thread t = Thread.currentThread();
        if (writingThread != t) throw new IllegalMonitorStateException("無寫鎖");
        if (--writeCount == 0) writingThread = null;
        notifyAll();
    }
}

鍵值升級/降級

  • 讀升寫:僅當當前線程是唯一讀鎖持有者時允許(避免死鎖)
  • 寫降讀:持有寫鎖時直接允許獲取讀鎖(安全,無其他線程持鎖)

四、JDK ReentrantReadWriteLock 核心源碼邏輯

基於 AQS (AbstractQueuedSynchronizer) 同步器:

  • 狀態位分割:高 16 位 = 讀鎖計數,低 16 位 = 寫鎖計數
  • ReadLock:共享模式獲取,tryAcquireShared
  • WriteLock:獨佔模式獲取,tryAcquire
  • 公平/非公平模式可選(構造器參數)
  • 支持 Condition(僅寫鎖支持)

核心方法簽名


public class ReentrantReadWriteLock implements ReadWriteLock, Serializable {
    public ReentrantReadWriteLock() { this(false); }
    public ReentrantReadWriteLock(boolean fair) { ... }
    
    public ReentrantReadWriteLock.ReadLock readLock() { ... }
    public ReentrantReadWriteLock.WriteLock writeLock() { ... }
    
    public static class ReadLock implements Lock, Serializable { ... }
    public static class WriteLock implements Lock, Serializable { ... }
}

五、標準使用範式


private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private final Map cache = new HashMap<>();

// 讀操作
public String get(String key) {
    readLock.lock();
    try {
        return cache.get(key);
    } finally {
        readLock.unlock();  // 必須在 finally 中釋放
    }
}

// 寫操作
public void put(String key, String value) {
    writeLock.lock();
    try {
        cache.put(key, value);
    } finally {
        writeLock.unlock();
    }
}

// 寫降讀:持有寫鎖期間讀取(避免釋放寫鎖後被其他寫搶佔)
public String putIfAbsentAndGet(String key, String value) {
    writeLock.lock();
    try {
        String old = cache.get(key);
        if (old == null) cache.put(key, value);
        // 寫降讀:獲取讀鎖(不會阻塞)
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    } finally {
        writeLock.unlock();
    }
}

六、性能特徵與選型建議

場景推薦鎖理由
讀多寫少、讀操作耗時長ReentrantReadWriteLock讀並發無阻塞,吞吐遠超互斥鎖
讀寫均衡、臨界區極短ReentrantLock / synchronizedRWLock 開銷大,反而較慢
高併發緩存、配置熱加載ReadWriteLock / StampedLockStampedLock 樂觀讀更快
需要 Condition 等待條件ReentrantLock / RWLock.writeLock()ReadLock 不支持 Condition

七、StampedLock:JDK 8+ 更優替代者

  • 三種模式:寫鎖、悲觀讀鎖、樂觀讀鎖
  • 樂觀讀:無鎖讀取,validate(stamp) 驗證無寫發生則成功,否則升級悲觀讀
  • 非重入,無 Condition,API 較複雜
  • 高讀併發場景性能碾壓 ReentrantReadWriteLock

private final StampedLock sl = new StampedLock();

// 樂觀讀
public String get(String key) {
    long stamp = sl.tryOptimisticRead();
    String val = cache.get(key);
    if (!sl.validate(stamp)) {  // 有寫發生
        stamp = sl.readLock();  // 升級悲觀讀
        try { return cache.get(key); }
        finally { sl.unlockRead(stamp); }
    }
    return val;
}

八、最佳實踐清單

  1. 鎖對象 final 修飾,防止引用變更
  2. 釋放鎖必須在 finally 塊,防異常導致死鎖
  3. 縮小臨界區:鎖內僅放線程安全核心邏輯
  4. 避免鎖內調用外部方法/IO/睡眠
  5. 讀寫鎖不支持 Condition,需等待條件用寫鎖或單獨 ReentrantLock
  6. 高版本 JDK 優先評估 StampedLock 或 ConcurrentHashMap 替代

讀寫鎖是「共享-排他」並發控制的經典模式。理解其狀態機、重入規則、升降級邏輯與 AQS 實現原理,配合 StampedLock 樂觀讀優化,能精準解決高讀併發場景的性能瓶頸。工程實踐中,優先考慮無鎖數據結構(ConcurrentHashMap/CopyOnWriteArrayList),鎖是最後手段。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved