為何要有讀寫鎖
ReentrantLock鎖和其他鎖基本上都是排他鎖,排他鎖指的是在同一時刻隻允許一個線程持有鎖,通路共享資源。雖然ReentrantLock支援同一個線程重入,但是允許重入的是同一個線程。是以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類型的變量的劃分方式。
如上個這個圖寫狀态=1,表示目前線程擷取了寫鎖,讀狀态=2,表示該線程同時擷取了兩次讀鎖。使用高低位來表示讀寫狀态,那麼狀态的擷取和設值是如何實作的呢?
寫狀态的get()
此時假設同步狀态,也就是32的整型變量的值為S,寫狀态的擷取方式為S&0x0000FFFF,這種辦法會保留低16位的同時抹去高16位,也就能計算出寫狀态的擷取情況。
源碼分析
源碼分析主要分為四塊,分别是ReentrantReadWriteLock中一個int分為兩半計算方式、寫鎖的擷取與釋放、讀鎖的擷取與釋放以及鎖降級這幾個方面來展開。
1、一個int分為兩半計算方式
int拆分為高16位和低16位的計算是在ReentrantReadWriteLock的内部類在Sync中實作的,其主要核心如下
2、寫鎖的擷取與釋放
WriteLock寫鎖是一個支援重入的排它鎖,它的擷取與釋放通過tryAcquire(int arg)和tryRelease(int arg)來實作,擷取到寫鎖的條件是,目前讀鎖未被擷取或者目前線程是持有寫鎖的線程,否則擷取失敗進入等待狀态。
寫鎖擷取
3、讀鎖的擷取與釋放
讀鎖的擷取與釋放相比寫鎖的擷取與釋放相對來說要複雜一些,因為它是支援重入的共享鎖,它能被多個線程同時擷取,是以我們不僅需要記錄擷取讀鎖的每一個線程,同時需要記錄每個線程對于讀鎖的重入次數,是以我們首先來看讀鎖的重入計數。
讀鎖的重入計數
讀鎖的重入計數,巧妙的使用每個線程自己來記錄,通過存在在ThreadLocal中的HoldCounter中的變量count來增加和減少,其是線程隔離的是以也是線程安全的,具體實作在Sync中。
讀鎖擷取
/*
* 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);
}