天天看點

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

為何要有讀寫鎖

ReentrantLock鎖和其他鎖基本上都是排他鎖,排他鎖指的是在同一時刻隻允許一個線程持有鎖,通路共享資源。雖然ReentrantLock支援同一個線程重入,但是允許重入的是同一個線程。是以ReentrantReadWriteLock是為了支援多個線程在同一個時刻對于鎖的通路孕育而生的。

讀寫鎖—簡單介紹

讀寫鎖ReentrantReadWriteLock允許多個線程在同一時刻對于鎖的通路。但是,寫線程擷取寫鎖時,所有的讀線程和其他寫線程均會被阻塞。讀寫鎖是通過維護一對鎖(一個讀鎖、一個寫鎖)來實作的,讀寫鎖在很多讀多于寫得場景中有很大的性能提升。舉個例子,當我們對資料庫中的一條記錄進行讀取時,不應該阻塞其他讀取這條資料的線程,而如果是有線程對該記錄進行寫操作,資料庫就應該阻止其他線程對這條資料的讀取和寫入,這種場景就可以用類似讀寫鎖的方式來處理。

特性

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

使用示例

通過ReentrantReadWriteLock來實作一個線程安全的簡單記憶體緩存設計,通過給緩存擷取get(String key)方法加上讀鎖,允許其他線程在同一個時刻進行資料讀取;給put(String key,String value)方法加上寫鎖,禁止在對緩存寫入的時候其他線程對于緩存的讀取和寫入操作。

package com.lizba.p6;

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

/**
 * <p>
 *      使用ReentrantReadWriteLock實作一個簡單的線程安全基于map的緩存設計
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/22 22:11
 */
public class CacheDemo {

    /** 存儲資料容器 */
    private static Map<String, Object> cache = new HashMap<>();
    /** 讀寫鎖ReentrantReadWriteLock */
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    /** 讀鎖 */
    static Lock readLock = lock.readLock();
    /** 寫鎖 */
    static Lock writeLock = lock.writeLock();

    /**
     * 擷取資料,使用讀鎖加鎖
     * @param key
     * @return
     */
    public static Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * 設定key&value,使用寫鎖,禁止其他線程的讀取和寫入
     * @param key
     * @param value
     * @return
     */
    public static void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 清空緩存
     */
    public static void flush() {
        writeLock.lock();
        try {
            cache.clear();
        } finally {
            writeLock.unlock();
        }
    }

}
      

實作分析

ReentrantReadWriteLock是基于AQS來實作的,我們知道AQS是以單個int類型的原子變量來表示其狀态的,那麼一個狀态如何即表示讀鎖又表示寫鎖呢?觀察源碼發現(後續會有源碼解析)ReentrantReadWriteLock通過使用一個32位的整型變量的高16位表示讀,低16位表示寫來維護讀線程和寫線程對于鎖的擷取情況。此時部分讀者可能又會産生一個疑問,寫鎖是互斥的,隻支援有一個線程持有寫鎖,用低16位記錄同一個線程對寫鎖的多次重入是沒問題的;那麼對于允許多個線程擷取的讀鎖,高16位又是如何維護持有鎖的線程數和單個線程對讀鎖的擷取次數的呢?其實這裡引入了ThreadLocal來記錄單個線程自己對于讀鎖的擷取次數,高16位存儲的是擷取讀鎖的線程的個數。這裡隻是簡單說下疑問,後續源碼會有詳細分析。我們先來看看這個int類型的變量的劃分方式。

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

如上個這個圖寫狀态=1,表示目前線程擷取了寫鎖,讀狀态=2,表示該線程同時擷取了兩次讀鎖。使用高低位來表示讀寫狀态,那麼狀态的擷取和設值是如何實作的呢?

寫狀态的get()

此時假設同步狀态,也就是32的整型變量的值為S,寫狀态的擷取方式為S&0x0000FFFF,這種辦法會保留低16位的同時抹去高16位,也就能計算出寫狀态的擷取情況。

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解
讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

源碼分析

源碼分析主要分為四塊,分别是ReentrantReadWriteLock中一個int分為兩半計算方式、寫鎖的擷取與釋放、讀鎖的擷取與釋放以及鎖降級這幾個方面來展開。

1、一個int分為兩半計算方式

int拆分為高16位和低16位的計算是在ReentrantReadWriteLock的内部類在Sync中實作的,其主要核心如下

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解
2、寫鎖的擷取與釋放

WriteLock寫鎖是一個支援重入的排它鎖,它的擷取與釋放通過tryAcquire(int arg)和tryRelease(int arg)來實作,擷取到寫鎖的條件是,目前讀鎖未被擷取或者目前線程是持有寫鎖的線程,否則擷取失敗進入等待狀态。

寫鎖擷取

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解
讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

3、讀鎖的擷取與釋放

讀鎖的擷取與釋放相比寫鎖的擷取與釋放相對來說要複雜一些,因為它是支援重入的共享鎖,它能被多個線程同時擷取,是以我們不僅需要記錄擷取讀鎖的每一個線程,同時需要記錄每個線程對于讀鎖的重入次數,是以我們首先來看讀鎖的重入計數。

讀鎖的重入計數

讀鎖的重入計數,巧妙的使用每個線程自己來記錄,通過存在在ThreadLocal中的HoldCounter中的變量count來增加和減少,其是線程隔離的是以也是線程安全的,具體實作在Sync中。

讀寫鎖-ReentrantReadWriteLock源碼分析與圖解

讀鎖擷取

/*
  * tryAcquireShared方法實作在Sync中,unused如其定義未被使用
  */
protected final int tryAcquireShared(int unused) {
    // 擷取目前線程
    Thread current = Thread.currentThread();
    // 擷取同步狀态值
    int c = getState();
    // exclusiveCount(c)擷取寫鎖的值,如果不為0表示寫鎖已被持有
    // getExclusiveOwnerThread(),如果是寫鎖則需要判斷目前線程和持有寫鎖(互斥鎖)的線程是否相等
    // 因為寫鎖被允許擷取讀鎖,如果不是同一個線程則直接傳回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 擷取讀鎖持有的線程數
    int r = sharedCount(c);
    // readerShouldBlock() 有兩個實作分别是FairSync和NonfairSync,兩種的擷取鎖政策實作不一樣,用于判斷是否擷取讀鎖
    // r < MAX_COUNT 判斷讀鎖擷取線程數是否小于最大值
    // compareAndSetState(c, c + SHARED_UNIT) 嘗試擷取讀鎖
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果擷取所成功
        // 目前線程為第一個擷取鎖的線程,并且是第一次
        if (r == 0) {
            // 設定目前線程為firstReader
            firstReader = current;
            // 設定目前線程重入讀鎖的次數為1
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {  // 如果不是第一次,但是是第一個擷取讀鎖的線程
            // 目前線程重入讀鎖的次數為累加1
            firstReaderHoldCount++;
        } else {
            // 擷取緩存cachedHoldCounter
            HoldCounter rh = cachedHoldCounter;
            // 如果緩存為空,或者緩存的線程ID與目前線程ID不一緻
            if (rh == null || rh.tid != getThreadId(current))
                // 從ThreadLocalHoldCounter中讀取目前線程的讀鎖重入次數,設定緩存
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) // 緩存存在,但是重入次數為空
                // 設定目前線程readHolds中的HoldCounter
                readHolds.set(rh);
            // 重入次數+1
            rh.count++;
        }
        return 1;
    }
    // 擷取讀鎖失敗,自旋重試
    return fullTryAcquireShared(current);
}
      
讀寫鎖-ReentrantReadWriteLock源碼分析與圖解
讀寫鎖-ReentrantReadWriteLock源碼分析與圖解