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

Java 多線程深度解析:讀寫鎖實現原理、重入、升降級與條件變量

     編輯:Java 綜合教學

Java 多線程深度解析:讀寫鎖實現原理、重入、升降級與條件變量

JDK 5+ 在 java.util.concurrent.locks 提供 ReentrantReadWriteLock,但理解其底層原理對掌握併發編程至關重要。本文從零手寫一個功能完整的讀寫鎖,涵蓋基本互斥、重入、讀升寫、寫降讀、條件變量防信號丟失/虛假喚醒。

基本讀寫互斥規則

  • 讀鎖:無線程持有寫鎖、無線程請求寫鎖 → 允許並發讀
  • 寫鎖:無線程持有讀鎖、無線程持有寫鎖 → 獨佔寫
  • 寫請求優先級高於讀,防止寫線程饑餓

基礎實現(非重入)

public class ReadWriteLock {
    private int readers = 0, writers = 0, writeRequests = 0;

    public synchronized void lockRead() throws InterruptedException {
        while (writers > 0 || writeRequests > 0) wait();
        readers++;
    }
    public synchronized void unlockRead() {
        readers--;
        notifyAll();
    }
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        while (readers > 0 || writers > 0) wait();
        writeRequests--;
        writers++;
    }
    public synchronized void unlockWrite() {
        writers--;
        notifyAll();
    }
}

關鍵點:notifyAll() 而非 notify(),避免喚醒的讀線程因寫請求存在而再次等待,導致寫線程永不被喚醒(信號丟失)。

重入增強

讀鎖重入

  • 用 Map<Thread, Integer> 記錄每線程持有讀鎖計數
  • 允許已持有讀鎖的線程重入,即使有寫請求
private Map<Thread, Integer> readingThreads = new HashMap<>();
private boolean canGrantReadAccess(Thread t) {
    if (writers > 0) return false;
    if (isReader(t)) return true;           // 重入
    if (writeRequests > 0) return false;    // 寫優先
    return true;
}

寫鎖重入

  • 記錄持有寫鎖的線程 writingThread 及計數 writeAccesses
  • 僅允許持有寫鎖的線程重入
private Thread writingThread = null;
private int writeAccesses = 0;
private boolean canGrantWriteAccess(Thread t) {
    if (hasReaders()) return false;
    if (writingThread == null) return true;
    return isWriter(t); // 重入
}

鎖升降級

讀升寫

允許唯一讀線程升級為寫鎖:

private boolean isOnlyReader(Thread t) {
    return readingThreads.size() == 1 && readingThreads.containsKey(t);
}
private boolean canGrantWriteAccess(Thread t) {
    if (isOnlyReader(t)) return true; // 升級
    if (hasReaders()) return false;
    // ... 原有邏輯
}

寫降讀

持有寫鎖的線程直接獲取讀鎖(無其他線程持有鎖):

private boolean canGrantReadAccess(Thread t) {
    if (isWriter(t)) return true; // 降級
    if (writingThread != null) return false;
    // ... 原有邏輯
}

完整可重入實現

public class ReadWriteLock {
    private final Map<Thread, Integer> readingThreads = new HashMap<>();
    private int writeAccesses = 0, writeRequests = 0;
    private Thread writingThread = null;

    public synchronized void lockRead() throws InterruptedException {
        Thread t = Thread.currentThread();
        while (!canGrantReadAccess(t)) wait();
        readingThreads.put(t, getReadAccessCount(t) + 1);
    }
    public synchronized void unlockRead() {
        Thread t = Thread.currentThread();
        if (!isReader(t)) throw new IllegalMonitorStateException();
        int c = getReadAccessCount(t);
        if (c == 1) readingThreads.remove(t);
        else readingThreads.put(t, c - 1);
        notifyAll();
    }
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread t = Thread.currentThread();
        while (!canGrantWriteAccess(t)) wait();
        writeRequests--;
        writeAccesses++;
        writingThread = t;
    }
    public synchronized void unlockWrite() {
        if (!isWriter(Thread.currentThread())) 
            throw new IllegalMonitorStateException();
        if (--writeAccesses == 0) writingThread = null;
        notifyAll();
    }
    // 輔助方法:canGrantReadAccess, canGrantWriteAccess, isReader, isWriter, 
    // hasReaders, isOnlyReader, getReadAccessCount, hasWriteRequests
}

條件變量最佳實踐

臨界區可能拋出異常,必須在 finally 塊釋放鎖:

lock.lockWrite();
try {
    // 臨界區代碼
} finally {
    lock.unlockWrite();
}

否則異常導致鎖永不釋放,造成死鎖。可重入特性僅在同一線程後續獲取鎖時自動解鎖,不可依賴。

總結

  • 讀寫鎖核心:讀讀共享、讀寫互斥、寫寫互斥、寫優先防饑餓
  • 重入需線程級計數器,讀重入允許寫請求存在時進入,寫重入僅限持有者
  • 升降級:唯一讀者可升寫,寫者可降讀
  • notifyAll() + while 循環檢查條件,防信號丟失與虛假喚醒
  • 生產代碼直接使用 ReentrantReadWriteLock,但原理必須精通
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved