系列傳送門:
- Java併發包原始碼學習系列:AbstractQueuedSynchronizer
- Java併發包原始碼學習系列:CLH同步佇列及同步資源獲取與釋放
- Java併發包原始碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別
- Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解
ReadWriteLock讀寫鎖概述
我們之前說到,ReentrantLock是獨佔鎖,某一時刻只有一個執行緒可以獲取該鎖,而實際上會存在很多讀多寫少的場景,而讀操作本身並不會存在資料競爭問題,如果使用獨佔鎖,可能會導致其中一個讀執行緒使其他的讀執行緒陷入等待,降低效能。
針對這種讀多寫少的場景,讀寫鎖應運而生。讀寫鎖允許同一時刻有多個讀執行緒訪問,但在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。我們先來看看Java中的讀寫鎖頂級介面吧,位於:java.util.concurrent.locks
包下:
public interface ReadWriteLock {
// 讀鎖
Lock readLock();
// 寫鎖
Lock writeLock();
}
相信你會一下子就明白,讀寫鎖其實就是維護了一對鎖,一個寫鎖一個讀鎖,通過讀寫分離的策略,允許多個執行緒同時獲取讀鎖,大大提高併發性。
讀寫鎖案例
JavaDoc文件寫的非常詳細,給我們舉了一個ReentrantReadWriteLock的使用例子,我們直接來看看:
class CachedData {
Object data;
volatile boolean cacheValid;
// 建立讀寫鎖例項
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
// 獲取讀鎖
rwl.readLock().lock();
// 快取失效的情況
if (!cacheValid) {
// 釋放掉讀鎖,必須!在獲取寫鎖之前給讀鎖釋放了
rwl.readLock().unlock();
// 獲取寫鎖
rwl.writeLock().lock();
try {
// 重新檢查狀態,因為在等待寫鎖的過程中,可能前面有其他寫執行緒執行過了
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 持有寫鎖的情況下,獲取讀鎖的,稱為 “鎖降級”
rwl.readLock().lock();
} finally {
// 釋放寫鎖,此時還剩一個讀鎖
rwl.writeLock().unlock();
}
}
try {
use(data);
} finally {
// 釋放讀鎖
rwl.readLock().unlock();
}
}
}
稍微總結一下,詳細的在後面的解析部分:
ReentrantReadWriteLock讀寫鎖分為讀鎖和寫鎖,讀鎖是共享鎖,寫鎖是獨佔鎖。
持有寫鎖的執行緒可以繼續獲取讀鎖,稱為鎖降級。
ReentrantReadWriteLock架構總覽
ReentrantReadWriteLock是ReadWriteLock的實現,其實看到這個名兒:可重入的讀寫鎖,我們就大概可以猜測一下它的意思了。除了實現了readLock()
和writeLock()
兩個方法之外,還提供了一些重要方法,我們待會會一一解析。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** 內部維護ReadLock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 內部維護WriteL */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 讀、寫鎖公用一個AQS的Sync的例項 */
final Sync sync;
/** 預設使用非公平模式 */
public ReentrantReadWriteLock() {
this(false);
}
/** 初始化讀鎖和寫鎖例項 */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
/**
* AQS的實現
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}
/**
* Sync 非公平版本的實現
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Sync 公平版本的實現
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
/**
* 可以通過ReentrantReadWriteLock#readLock方法得到一個讀鎖例項
*/
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 可以看到讀鎖使用的是共享模式
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
//...省略tryLock、lockInterruptibly等方法
}
/**
* 可以通過ReentrantReadWriteLock#writeLock方法獲得一個寫鎖例項
*/
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 可以看到讀鎖使用的是獨佔模式
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
//...省略tryLock、lockInterruptibly等方法
}
我們大概總結一下:
- ReentrantReadWriteLock內部維護了ReadLock和WriteLock兩個內部類,他們都委託Sync實現具體功能【Sync是AQS的實現,這個之前講的非常清楚咯】。
- 與ReentrantLock一樣,也提供了公平與非公平兩種實現:FairSync和NonfairSync,他們是Sync的實現類,兩種區別參見:公平與非公平策略的差異
- ReadLock和WriteLock例項可以通過
readLock()
和writeLock()
兩個方法獲得。 - ReadLock使用了共享模式、WriteLock使用了獨佔模式,兩者區別參見:Java併發包原始碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別。
Sync重要欄位及內部類表示
我們在學習AQS的時候說到過,AQS的關鍵就是同步狀態欄位state,例如以ReentrantLock為例,它的state為0表示鎖空閒,為1表示有鎖被獲取,大於1表示鎖被同一個執行緒重入。
但已知讀寫鎖需要維護兩種狀態,僅用一個整型變數state如何表示呢?讀寫鎖利用按位切割的思想,巧妙地將state分割為兩部分:
- 高16位:表示讀狀態,代表讀鎖的獲取次數【包括重入次數】,由於共享模式,可以有多個執行緒獲取鎖,且可以重入。
- 低16位:表示寫狀態,代表寫鎖的可重入次數,獨佔模式,只有一個執行緒可以獲得寫鎖,但是可以表示可重入次數。
注意區別這兩者的區別。
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
// 共享鎖狀態單位值 65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 共享鎖執行緒最大個數 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 排他鎖掩碼 65535 二進位制表示 15個1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 返回讀鎖的獲取次數【包括重入次數】 無符號補0右移16位,其實就是獲取高16位 */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 返回寫鎖可重入次數 將高16位抹去,其實就是獲取低16位 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
- sharedCount:無符號補0右移16位,其實就是獲取高16位。
- exclusiveCount:將高16位抹去,其實就是獲取低16位。
// 記錄每個執行緒持有的讀鎖數量
static final class HoldCounter {
// 持有的讀鎖數
int count = 0;
// Use id, not reference, to avoid garbage retention
// 執行緒id
final long tid = getThreadId(Thread.currentThread());
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
* ThreadLocal的子類
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 每個執行緒都需要記錄獲取讀鎖的次數,判斷是否重入
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// ThreadLocalHoldCounter繼承ThreadLocal
// 存放除去第一個獲取讀鎖執行緒外的其他執行緒獲取讀鎖的可重入次數
private transient ThreadLocalHoldCounter readHolds;
// 記錄最後一個獲取讀鎖的執行緒獲取讀鎖的可重入次數
private transient HoldCounter cachedHoldCounter;
// 記錄第一個獲取到讀鎖的執行緒
private transient Thread firstReader = null;
// 記錄第一個獲取到讀鎖的執行緒獲取讀鎖的可重入次數
private transient int firstReaderHoldCount;
Sync() {
// 初始化readHolds
readHolds = new ThreadLocalHoldCounter();
// 保證readHolds的記憶體可見性
setState(getState()); // ensures visibility of readHolds
}
寫鎖的獲取
ReentrantReadWriteLock中的寫鎖通過WriteLock實現。
void lock()
寫鎖是獨佔鎖,某一時刻只有一個執行緒可以獲取該鎖。
- 如果當前沒有執行緒獲取到讀鎖寫鎖,則當前執行緒可以獲取到寫鎖然後返回。
- 如果當前已經有執行緒獲取到到讀鎖和寫鎖,當前請求寫鎖的執行緒會被阻塞掛起。
寫鎖是可重入鎖,如果當前執行緒已經獲取該鎖,再次獲取只是簡單地把可重入次數+1後直接返回。
// ReentrantReadWriteLock.WriteLock#lock
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
sync.acquire(1);
}
}
// AQS # acquire
public final void acquire(int arg) {
// 呼叫子類實現的tryAcquire,如果位false,則加入阻塞佇列,阻塞
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// ReentrantReadWriteLock.Sync#tryAcquire
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// c != 0表示讀鎖或者寫鎖已經被某個執行緒獲取了
if (c != 0) {
// c != 0 && w == 0表示有執行緒獲取了讀鎖,share count此時不為0。
// c != 0 && w != 0並且當前執行緒不是寫鎖擁有者,返回false
// 意思是隻要有讀鎖或寫鎖被佔用,這次獲取寫鎖就會失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//走到這裡說明當前執行緒就是已經獲取寫鎖的,判斷可重入的次數是否超過了最大值
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 設定可重入的次數,不需要CAS,因為走到這裡必然是寫鎖重入
setState(c + acquires);
return true;
}
// 走到這,表示 c==0,此時為第一個執行緒嘗試獲取寫鎖。
// 如果寫鎖不需要block,進行cas操作,成功則表示獲取成功
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 經過前面的步驟之後,到這一步,才設定鎖的持有者為當前執行緒
setExclusiveOwnerThread(current);
return true;
}
}
boolean writerShouldBlock()
writerShouldBlock方法實現,公平與非公平有差異:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
// 返回是否存在前驅節點,會先看看前面有沒有在排隊的
return hasQueuedPredecessors();
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 總是返回false,直接去cas
final boolean writerShouldBlock() {
return false; // writers can always barge
}
}
很明顯了,對於非公平鎖來說,該方法永遠返回false,表示一定會且直接會走到compareAndSetState(c, c + acquires)
這一步,通過CAS嘗試獲取寫鎖,獲取成功就設定狀態,之後當前執行緒會被設定為鎖的持有者,失敗則返回false。
意思是:非公平模式下,會直接嘗試cas去搶這個寫鎖,搶不到再排隊;而對於公平模式來說,如果阻塞佇列中,當前執行緒存在前驅節點,就放棄CAS爭奪寫鎖的過程。
void lockInterruptibly()
類似於ReentrantLock的lockInterruptibly()方法,當其他執行緒呼叫了該執行緒的interrupt()方法中斷了當前執行緒時,當前執行緒就會丟擲InterruptedException異常。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//AQS
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
boolean tryLock()
嘗試獲取寫鎖,如果當前沒有其他執行緒持有寫鎖或讀鎖,則當前執行緒獲取寫鎖會成功,返回true。
如果當前已經有其他執行緒持有寫鎖或讀鎖則該方法直接返回false,且當前執行緒並不會被阻塞。
如果當前執行緒已經持有了該寫鎖則簡單增加AQS的狀態值後直接返回true。
public boolean tryLock( ) {
return sync.tryWriteLock();
}
// AQS
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
其實和lock方法的邏輯大差不大,只是採用lock方法的非公平鎖邏輯。
boolean tryLock(long timeout,TimeUnit unit)
類似於ReentrantLock的tryLock(long timeout,TimeUnit unit)方法。
嘗試獲取寫鎖,如果獲取失敗會將當前執行緒掛起指定時間,時間到了之後當前執行緒被啟用,如果還是沒有獲取到鎖,就返回false。
另外,該方法會對中斷進行的響應,如果其他執行緒呼叫了當前執行緒的interrupt()方法,響應中斷,丟擲異常。
寫鎖的釋放
void unlock()
嘗試釋放鎖,如果當前執行緒持有該鎖,呼叫該方法會讓該執行緒對該執行緒持有的AQS狀態減1,如果減1之後當前狀態值為0,則當前執行緒會釋放該鎖。
如果當前執行緒沒有持有該鎖而呼叫了該方法,丟擲IllegalMonitorStateException異常。
public void lock() {
sync.acquireShared(1);
}
// AQS
public final boolean release(int arg) {
// 嘗試釋放鎖
if (tryRelease(arg)) {
Node h = head;
// 如果釋放成功,叫醒後繼節點
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantReadWriteLock.Sync#tryAcquire
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
// 當前執行緒沒有持有該鎖而呼叫了該方法
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 判斷一下是不是需要釋放鎖了
boolean free = exclusiveCount(nextc) == 0;
// 清空一下
if (free)
setExclusiveOwnerThread(null);
// state沒有到0,僅僅是設定state而已
setState(nextc);
// 如果寫鎖全部釋放,返回true,上面的方法就會喚醒之後的節點
return free;
}
}
讀鎖的獲取
ReentrantReadWriteLock中的讀鎖通過ReadLock實現,ps:讀鎖的獲取與釋放相對於寫鎖來說,較為複雜。
void lock()
// ReentrantReadWriteLock.ReadLock#lock
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
sync.acquireShared(1);
}
}
// AQS # acquireShared
public final void acquireShared(int arg) {
// 呼叫子類實現的tryAcquireShared,如果為false,則加入阻塞佇列,阻塞
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// ReentrantReadWriteLock.Sync#tryAcquire
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
// 獲取當前狀態值
int c = getState();
if (exclusiveCount(c) != 0 && // 說明有執行緒持有寫鎖
getExclusiveOwnerThread() != current) // 並且不是當前執行緒持有寫鎖
return -1; // 失敗
//----- 這裡提一嘴,上面如果持有寫鎖的是自己,就還是可以獲取讀鎖的 -----//
// 獲取讀鎖計數
int r = sharedCount(c);
// 讀鎖獲取是否需要阻塞,若不成功將會進入fullTryAcquireShared進行重試
if (!readerShouldBlock() &&
r < MAX_COUNT && // 判斷讀鎖獲取次數是否溢位
// 嘗試將高16位+1,低16位不變,如果獲取成功則表示獲取到了讀鎖
compareAndSetState(c, c + SHARED_UNIT)) {
// ----- 能走到這裡表示當前執行緒獲取讀鎖成功 ----- //
// r==0表示第一個執行緒獲取讀鎖 ,也有可能之前有執行緒但被釋放了,當前自然就是第一個啦
if (r == 0) {
firstReader = current; // 記錄一下firstReader【每次將讀鎖獲取次數從0變成1】
firstReaderHoldCount = 1; // 記錄一下持有的讀鎖數量 1
// 來到這裡 c != 0 且 firstReader == current ,表示firstReader可重入了
} else if (firstReader == current) {
firstReaderHoldCount++; // 直接計數加1
} else {
// cachedHoldCounter 使用來快取最後一個獲取讀鎖的執行緒的,之後用rh表示
HoldCounter rh = cachedHoldCounter;
// 如果rh還沒快取 或者 存的不是當前執行緒
if (rh == null || rh.tid != getThreadId(current))
// 那就更新一下cachedHoldCounter 為當前執行緒的HoldCounter
cachedHoldCounter = rh = readHolds.get();
// 走到這裡,說明快取的是當前執行緒,但是count是0
else if (rh.count == 0)
readHolds.set(rh);
// count 加1
rh.count++;
}
return 1; // 大於0表示獲取到了共享鎖
}
// 類似tryAcquireShared,自旋獲取,這裡失敗的話,就得進阻塞佇列去了嗷
return fullTryAcquireShared(current);
}
}
boolean readerShouldBlock()
readerShouldBlock方法實現,公平與非公平有差異:
static final class FairSync extends Sync {
final boolean readerShouldBlock() {
// 看看阻塞佇列中是否已經有其他元素在排隊
return hasQueuedPredecessors();
}
}
static final class NonfairSync extends Sync {
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
// 判斷阻塞佇列中 第一個節點是否是來獲取寫鎖的,如果是的話,讓這個寫鎖先來。
return apparentlyFirstQueuedIsExclusive();
}
}
具體看下非公平鎖的實現,apparentlyFirstQueuedIsExclusive方法:
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && // 佇列是否為空
(s = h.next) != null && // 是否存在第一個元素
!s.isShared() && // 第一個元素是否正在嘗試獲取寫鎖
s.thread != null; // 該元素的執行緒是否為null
}
聯絡起來解釋:
!readerShouldBlock()
:如果佇列裡面存在一個元素,判斷第一個元素是不是正在嘗試獲取寫鎖,如果是的話,這個讓這個元素先來,它的優先順序很高。r < MAX_COUNT
:判斷當前獲取讀鎖的執行緒是否達到最大值。compareAndSetState(c, c + SHARED_UNIT)
:執行CAS操作將AQS狀態值的高16位值增加1。
其實就是:看看佇列裡面的第一個節點是不是在嘗試獲取寫鎖,如果是的話,就讓他先來。如果你在獲取讀鎖,那不好意思,乖乖地去CAS吧,看誰能搶到。
如果沒有獲取到讀鎖,會怎麼辦呢?進入fullTryAcquireShared邏輯看看:
int fullTryAcquireShared(Thread)
/**
* Full version of acquire for reads, that handles CAS misses
* and reentrant reads not dealt with in tryAcquireShared.
*/
final int fullTryAcquireShared(Thread current) {
/*
* 這段程式碼和tryAcquireShared部分冗餘,但總體更加簡單
*/
HoldCounter rh = null;
// 自旋
for (;;) {
int c = getState();
// 已經有執行緒獲取了寫鎖
if (exclusiveCount(c) != 0) {
// 且獲取寫鎖的執行緒不是當前執行緒,那就直接進隊,如果是當前執行緒,走到cas去,鎖降級的過程
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 走到這一步,表示寫鎖沒被佔有,且阻塞佇列中有其他執行緒在等待
// firstReader執行緒重入讀鎖,直接快進到下面的cas
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
// cachedHoldCounter 沒有快取或快取的不是當前執行緒
if (rh == null || rh.tid != getThreadId(current)) {
// 如果當前執行緒從來沒有初始化ThreadLocal中的值,get方法會進行初始化
rh = readHolds.get();
// 表示上一行程式碼是初始化的,執行remove
if (rh.count == 0)
readHolds.remove();
}
}
// 排隊
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// cas操作成功,表示獲取讀鎖了,接下來設定firstReader或firstReaderHoldCount
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 將cachedHoldCounter設定為當前執行緒
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
接下來讀鎖的幾個方法和寫鎖其實差不太多,原始碼就不貼了,感興趣的小夥伴可以自己看看。
void lockInterruptibly()
類似於lock()方法,區別在於,該方法能夠中斷響應,當其他執行緒呼叫該執行緒的interrupt()方法中斷了當前執行緒時,當前執行緒丟擲InterruptedException異常。
boolean tryLock()
嘗試讀取鎖,如果當前沒有其他執行緒持有寫鎖,則當前執行緒會獲取讀鎖成功,返回true。
如果當前已經有其他執行緒持有寫鎖,則直接返回false,不會阻塞。
如果當前執行緒已經持有了該讀鎖,則利用AQS將state的高16位加1,返回true。
boolean tryLock(long timeout,TimeUnit unit)
類似於tryLock,不同的是,設定了超時時間,超時時間到了,如果沒有讀取到讀鎖,直接返回false。
可中斷響應,當其他執行緒呼叫該執行緒的interrupt()方法中斷了當前執行緒時,當前執行緒丟擲InterruptedException異常。
讀鎖的釋放
void unlock()
public void unlock() {
sync.releaseShared(1);
}
// AQS
public final boolean releaseShared(int arg) {
// 如果tryReleaseShared返回true,釋放一個由於獲取寫鎖而被阻塞的執行緒
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// 如果firstReaderHoldCount為1,這次解鎖之後,就會變成0了,將firstReader設定為null
if (firstReaderHoldCount == 1)
firstReader = null;
else
// 否則減1就可以
firstReaderHoldCount--;
} else {
// 判斷cacheHoldCounter是否快取的是當前執行緒,如果不是的話,需要從ThreadLocal中取。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 不再持有鎖了,呼叫remove,防止記憶體洩露
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 無限迴圈,保證CAS操作成功
for (;;) {
// 獲取狀態值
int c = getState();
int nextc = c - SHARED_UNIT;
// CAS 操作更新狀態值。CAS操作如果不成功,會一直迴圈
if (compareAndSetState(c, nextc))
// 如果更新成功,檢視當前狀態值是否為0,如果為0說明已經沒有讀執行緒佔用讀鎖
// 如果不為0,則說明還有其他執行緒持有讀鎖,返回false
return nextc == 0;
}
}
}
鎖降級的理解
鎖降級就意味著寫鎖是可以降級為讀鎖的,但是需要遵循先獲取寫鎖、獲取讀鎖在釋放寫鎖的次序。注意如果當前執行緒先獲取寫鎖,然後釋放寫鎖,再獲取讀鎖這個過程不能稱之為鎖降級,鎖降級一定要遵循那個次序。
注意,作者Doug Lea並沒有說寫鎖更為高階,如果有執行緒持有讀鎖,那麼寫鎖獲取也需要等待,但原始碼中確實可以看出給寫鎖一些特殊照顧,如在非公平模式下,為了提高吞吐量,如果發現第一個節點是獲取寫鎖的執行緒,直接獲取成功。
鎖降級的部分,原始碼中是這樣體現的:
int c = getState();
// 已經有執行緒獲取了寫鎖
if (exclusiveCount(c) != 0) {
// 且獲取寫鎖的執行緒不是當前執行緒,那就直接進隊,如果是當前執行緒,走到cas去,鎖降級的過程
if (getExclusiveOwnerThread() != current)
return -1;
鎖降級中讀鎖的獲取是否必要?
假如當前執行緒 A 不獲取讀鎖而是直接釋放了寫鎖,這個時候另外一個執行緒 B 獲取了寫鎖,那麼這個執行緒 B 對資料的修改是不會對當前執行緒 A 可見的。
如果獲取了讀鎖,則執行緒B在獲取寫鎖過程中判斷如果有讀鎖還沒有釋放則會被阻塞,只有當前執行緒 A 釋放讀鎖後,執行緒 B 才會獲取寫鎖成功。
總結
-
ReentrantReadWriteLock底層使用AQS實現,利用AQS的狀態值的高16位表示獲取到讀鎖的個數,低16位標識獲取到寫鎖的執行緒的可重入次數,通過CAS對其進行操作實現讀寫分離,適用於讀多寫少的場景。
-
ReentrantReadWriteLock的三個特性:
- 公平性:支援公平和非公平兩種模式。
- 重入性:支援重入,讀寫鎖都支援最多65535個。
- 鎖降級:先獲取寫鎖,再獲取讀鎖,再釋放寫鎖,寫鎖就能降級為讀鎖。
-
讀寫鎖:讀寫鎖允許同一時刻有多個讀執行緒訪問,但在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。
參考閱讀
-
方騰飛:《Java併發程式設計的藝術》
-
DougLea:《Java併發程式設計實戰》