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

Java 讀寫鎖深度解析:ReentrantReadWriteLock 實戰與快取系統實作

     編輯:關於C語言

前言

在高併發 Java 應用中,ReentrantReadWriteLock(RRW)是實現「多讀單寫」同步控制的核心工具。相較於 synchronized 或 ReentrantLock 的排他鎖,RRW 允許多執行緒同時讀取共享資料,僅在寫入時獨佔,大幅提升讀多寫少場景的吞吐量。本文深入解析 RRW 核心機制、鎖降級模式、常見陷阱,並以生產級快取系統為例示範實戰應用。

一、核心機制與鎖規則

三大鎖規則

鎖類型排斥寫鎖排斥讀鎖自身並發
讀鎖✅ 是❌ 否✅ 多執行緒可同時持有
寫鎖✅ 是✅ 是❌ 單執行緒獨佔

關鍵特性

  • 可重入:同一執行緒可重複獲取同類型鎖(計數器機制)。
  • 鎖降級:寫鎖 → 讀鎖 合法(先獲取讀鎖再釋放寫鎖)。
  • 鎖升級:讀鎖 → 寫鎖 非法(會導致死鎖),必須先釋放讀鎖。
  • 公平性:建構函式可指定 fair=true,避免寫執行緒飢餓。

二、官方範例深度解讀:快取資料處理

class CachedData {
    Object data;
    volatile boolean cacheValid;
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();           // @1 獲取讀鎖
        try {
            if (!cacheValid) {           // 快取失效
                rwl.readLock().unlock(); // @4 必須先釋放讀鎖(升級前置條件)
                rwl.writeLock().lock();  // @2 獲取寫鎖
                try {
                    if (!cacheValid) {   // @3 Double-Check:另一執行緒可能已更新
                        data = computeExpensiveData();
                        cacheValid = true;
                    }
                } finally {
                    // 鎖降級:先獲取讀鎖,再釋放寫鎖
                    rwl.readLock().lock();  
                    rwl.writeLock().unlock(); // 釋放寫鎖,保持讀鎖
                }
            }
            use(data);                   // 讀取資料(持有讀鎖)
        } finally {
            rwl.readLock().unlock();     // 釋放讀鎖
        }
    }
}

流程逐步解析

  1. @1 獲取讀鎖:多執行緒可並發進入,若有寫鎖被持有則阻塞等待。
  2. 檢查快取有效性:若有效,直接讀取資料,釋放讀鎖結束——最佳路徑,無阻塞並發。
  3. @4 釋放讀鎖 → @2 獲取寫鎖:快取失效時,必須先釋放讀鎖才能獲取寫鎖(避免死鎖)。此時有短暫「無鎖窗口」。
  4. @3 Double-Check:獲取寫鎖後再次檢查,避免多執行緒同時進入寫入邏輯(經典雙重檢查鎖定模式)。
  5. 鎖降級:寫入完成後,先獲取讀鎖再釋放寫鎖,確保資料可見性且不讓其他寫執行緒插入。此為 RRW 獨有進階技巧。
  6. 使用資料並釋放讀鎖:持有讀鎖讀取,確保資料不被並發修改。

三、常見錯誤與避坑指南

❌ 錯誤 1:嘗試在持有讀鎖時獲取寫鎖(鎖升級)

// 錯誤!會導致死鎖
rwl.readLock().lock();
try {
    rwl.writeLock().lock(); // 執行緒永遠阻塞:自己持有的讀鎖阻擋寫鎖
} finally { ... }

正確:先 readLock().unlock(),再 writeLock().lock()。

❌ 錯誤 2:忽略 Double-Check

// 危險:多執行緒可能同時通過第一次檢查,重複執行昂貴計算
if (!cacheValid) {
    rwl.readLock().unlock();
    rwl.writeLock().lock();
    // 缺少第二次檢查
    data = compute(); // 可能重複計算
}

❌ 錯誤 3:finally 區塊拋出異常

finally {
    rwl.readLock().lock();   // 若這裡拋異常(罕見)
    rwl.writeLock().unlock(); // 寫鎖永不釋放 → 系統癱瘓
}

建議:鎖操作放在 try-finally 外層,或確保 lock() 不拋異常(RRW lock() 不拋受檢異常)。

❌ 錯誤 4:讀鎖區塊執行耗時操作

讀鎖應極短(記憶體讀取、快照複製)。若在讀鎖中執行 I/O、網路呼叫、複雜計算,會阻塞所有寫入者,降低系統回應性。

四、實戰:生產級快取系統實作

結合 HashMap、volatile、ReentrantReadWriteLock 實現執行緒安全、高效能的快取:

package com.example.cache;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 執行緒安全快取:讀多寫少場景優化
 *  - 讀操作並發無阻塞
 *  - 寫操作獨佔,雙重檢查避免重複計算
 *  - 鎖降級保證資料可見性
 */
public class CacheSystem {
    
    private final Map cache = new HashMap<>();
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Computable computable; // 昂貴計算介面
    
    public interface Computable {
        V compute(K key) throws Exception;
    }
    
    public CacheSystem(Computable computable) {
        this.computable = computable;
    }
    
    /**
     * 獲取資料:快取命中直接返回,未命中計算並存入
     */
    public V get(K key) throws Exception {
        // === 快速路徑:嘗試讀鎖獲取 ===
        rwl.readLock().lock();
        try {
            V value = cache.get(key);
            if (value != null) {
                return value; // 快取命中,直接返回
            }
        } finally {
            rwl.readLock().unlock();
        }
        
        // === 慢速路徑:需要計算 ===
        rwl.writeLock().lock();
        try {
            // Double-Check:另一執行緒可能已在寫鎖中完成計算
            V value = cache.get(key);
            if (value == null) {
                value = computable.compute(key); // 昂貴計算(持有寫鎖)
                cache.put(key, value);
            }
            // 鎖降級:確保寫入對後續讀取者可見
            rwl.readLock().lock();
            rwl.writeLock().unlock();
            return value;
        } finally {
            // 若未執行降級(如 compute 拋異常),確保釋放寫鎖
            if (rwl.writeLock().isHeldByCurrentThread()) {
                rwl.writeLock().unlock();
            }
            rwl.readLock().unlock(); // 釋放降級後的讀鎖
        }
    }
    
    /** 手動失效快取 */
    public void invalidate(K key) {
        rwl.writeLock().lock();
        try {
            cache.remove(key);
        } finally {
            rwl.writeLock().unlock();
        }
    }
    
    /** 清空快取 */
    public void clear() {
        rwl.writeLock().lock();
        try {
            cache.clear();
        } finally {
            rwl.writeLock().unlock();
        }
    }
}

使用範例

// 定義昂貴計算:模擬資料庫查詢
CacheSystem.Computable dbQuery = key -> {
    Thread.sleep(100); // 模擬延遲
    return "Value-for-" + key;
};

CacheSystem cache = new CacheSystem<>(dbQuery);

// 併發測試
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    final String key = "key-" + (i % 10); // 10 個不同 key,高命中率
    es.submit(() -> {
        try {
            String val = cache.get(key);
            System.out.println(Thread.currentThread().getName() + ": " + val);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}
es.shutdown();

五、效能調優建議

場景建議
極高讀併發、極少寫入使用 StampedLock (Java 8+) 乐观读模式,效能更高
需支援快取過期/淘汰整合 Caffeine、Guava Cache 成熟庫,勿自造輪子
分散式環境本地快取 + Redis 分散式快取,一致性透過 Pub/Sub 或 TTL 保證
寫入頻繁RRW 優勢消失,考慮 ConcurrentHashMap + computeIfAbsent 無鎖算法

六、StampedLock 現代化替代方案 (Java 8+)

StampedLock 提供三種模式:

  • 樂觀讀:無鎖讀取,驗證戳章無衝突才提交,極快但可能重試。
  • 悲觀讀:類似 RRW 讀鎖。
  • 寫鎖:獨佔寫入。
StampedLock sl = new StampedLock();

// 樂觀讀
long stamp = sl.tryOptimisticRead();
V value = cache.get(key);
if (!sl.validate(stamp)) { // 有寫入發生,降級悲觀讀
    stamp = sl.readLock();
    try { value = cache.get(key); } finally { sl.unlockRead(stamp); }
}

// 寫入
long stamp = sl.writeLock();
try { cache.put(key, value); } finally { sl.unlockWrite(stamp); }

注意:StampedLock 不可重入,使用需更謹慎。

總結

ReentrantReadWriteLock 是 Java 併發工具箱中處理「讀多寫少」共享資料的利器。掌握讀寫鎖互斥規則、鎖降級技巧、Double-Check 模式,並嚴格遵循 try-finally 釋放鎖的紀律,即可構建高效能、執行緒安全的快取與共享資料結構。現代專案中,優先評估 ConcurrentHashMap、StampedLock 或成熟快取庫(Caffeine),在確有必要時才手寫 RRW 邏輯。

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