一、概述
ReentrantLock是一個排他鎖,同一時間隻允許一個線程通路,而ReentrantReadWriteLock允許多個讀線程同時通路,但不允許寫線程和讀線程、寫線程和寫線程同時通路。相對于排他鎖,提高了并發性。在實際應用中,大部分情況下對共享資料(如緩存)的通路都是讀操作遠多于寫操作,這時ReentrantReadWriteLock能夠提供比排他鎖更好的并發性和吞吐量。
讀寫鎖内部維護了兩個鎖,一個用于讀操作,一個用于寫操作。所有 ReadWriteLock實作都必須保證 writeLock操作的記憶體同步效果也要保持與相關 readLock的聯系。也就是說,成功擷取讀鎖的線程會看到寫入鎖之前版本所做的所有更新。
ReentrantReadWriteLock支援以下功能:
1)支援公平和非公平的擷取鎖的方式;
2)支援可重入。讀線程在擷取了讀鎖後還可以擷取讀鎖;寫線程在擷取了寫鎖之後既可以再次擷取寫鎖又可以擷取讀鎖;
3)還允許從寫入鎖降級為讀取鎖,其實作方式是:先擷取寫入鎖,然後擷取讀取鎖,最後釋放寫入鎖。但是,從讀取鎖更新到寫入鎖是不允許的;
4)讀取鎖和寫入鎖都支援鎖擷取期間的中斷;
5)Condition支援。僅寫入鎖提供了一個 Conditon 實作;讀取鎖不支援 Conditon ,readLock().newCondition() 會抛出 UnsupportedOperationException。
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
二、源碼檢視
父接口定義
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock 也是基于AQS實作的,在其内部也是通過一個内部類Sync實作同步器AQS,同樣也是通過實作Sync實作公平鎖和非公平鎖。它的自定義同步器(繼承AQS)需要在同步狀态(一個整型變量state)上維護多個讀線程和一個寫線程的狀态,使得該狀态的設計成為讀寫鎖實作的關鍵。如果在一個整型變量上維護多種狀态,就一定需要“按位切割使用”這個變量,讀寫鎖将變量切分成了兩個部分,高16位表示讀,低16位表示寫。
自定義的同步器
abstract static class Sync extends AbstractQueuedSynchronizer{}
2.1、ReentrantReadWriteLock構造方法
//ReentrantReadWriteLock
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
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 writeLock0{return writerLock;}
public ReentrantReadWriteLock.ReadLock readLock0{return ReaderLock;}
2.1.1、ReadLock讀鎖實作
//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;//最後還是通過Sync内部類實作鎖
}
//它實作的是Lock接口,其餘的實作可以和ReentrantLock作對比,擷取鎖、釋放鎖等等
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}
2.1.2、WriteLock寫鎖實作
//ReentrantReadWriteLock$WriteLock
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
/**
* Constructor for use by subclasses
*
* @param lock the outer lock object
* @throws NullPointerException if the lock is null
*/
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;//通過Sync内部類實作鎖
}
//它實作的是Lock接口,其餘的實作可以和ReentrantLock作對比,擷取鎖、釋放鎖等等
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public String toString() {
Thread o = sync.getOwner();
return super.toString() + ((o == null) ?
"[Unlocked]" :
"[Locked by thread " + o.getName() + "]");
}
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
上面是對ReentrantReadWriteLock做了一個大緻的介紹,可以看到在其内部有好幾個内部類,實際上讀寫鎖内有兩個鎖——ReadLock、WriteLock,這兩個鎖都是實作自Lock接口,可以和ReentrantLock對比,而這兩個鎖的内部實作則是通過Sync,也就是同步器AQS實作的,這也可以和ReentrantLock中的Sync對比。
2.2、狀态确定方式
回顧一下AQS,其内部有兩個重要的資料結構——一個是同步隊列、一個則是同步狀态,這個同步狀态應用到讀寫鎖中也就是讀寫狀态,但AQS中隻有一個state整型來表示同步狀态,讀寫鎖中則有讀、寫兩個同步狀态需要記錄。是以,讀寫鎖将AQS中的state整型做了一下處理,它是一個int型變量一共4個位元組32位,那麼可以讀寫狀态就可以各占16位——高16位表示讀,低16位表示寫。
下面是《并發程式設計的藝術》給出的術語:如果在一個整型變量上維護多種狀态,就一定需要“按位切割使用”這個變量,讀寫鎖是将變量切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如圖所示。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
假設目前同步狀态值為S,get和set的操作如下:
(1)擷取寫狀态:
S&0x0000FFFF:将高16位全部抹去
(2)擷取讀狀态:
S>>>16:無符号補0,右移16位
(3)寫狀态加1:
S+1
(4)讀狀态加1:
S+(1<<16)即S + 0x00010000
在代碼層的判斷中,如果S不等于0,當寫狀态(S&0x0000FFFF),而讀狀态(S>>>16)大于0,則表示該讀寫鎖的讀鎖已被擷取。
2.3、寫鎖的擷取與釋放
WriteLock類中的lock和unlock方法:
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
AQS已經将擷取鎖的算法骨架搭好了,隻需子類實作tryAcquire(獨占鎖)和tryRelease。可以看到就是調用的獨占式同步狀态的擷取與釋放,是以真實的實作就是Sync的 tryAcquire和 tryRelease。
2.3.1、寫鎖的擷取
protected final boolean tryAcquire(int acquires) {
//目前線程
Thread current = Thread.currentThread();
//擷取狀态
int c = getState();
//寫線程數量(即擷取獨占鎖的重入數)
int w = exclusiveCount(c);
//目前同步狀态state != 0,說明已經有其他線程擷取了讀鎖或寫鎖
if (c != 0) {
// 目前state不為0,此時:如果寫鎖狀态為0說明讀鎖此時被占用傳回false;
// 如果寫鎖狀态不為0且寫鎖沒有被目前線程持有傳回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//判斷同一線程擷取寫鎖是否超過最大次數(65535),支援可重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//更新狀态
//此時目前線程已持有寫鎖,現在是重入,是以隻需要修改鎖的數量即可。
setState(c + acquires);
return true;
}
//到這裡說明此時c=0,讀鎖和寫鎖都沒有被擷取
//writerShouldBlock表示是否阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//設定鎖為目前線程所有
setExclusiveOwnerThread(current);
return true;
}
其中exclusiveCount方法表示占有寫鎖的線程數量,源碼如下:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
說明:直接将狀态state和(2^16 - 1)做與運算,其等效于将state模上2^16。寫鎖數量由state的低十六位表示。
從源代碼可以看出,擷取寫鎖的步驟如下:
(1)首先擷取c、w。c表示目前鎖狀态;w表示寫線程數量。然後判斷同步狀态state是否為0。如果state!=0,說明已經有其他線程擷取了讀鎖或寫鎖,執行(2);否則執行(5)。
(2)如果鎖狀态不為零(c != 0),而寫鎖的狀态為0(w = 0),說明讀鎖此時被其他線程占用,是以目前線程不能擷取寫鎖,自然傳回false。或者鎖狀态不為零,而寫鎖的狀态也不為0,但是擷取寫鎖的線程不是目前線程,則目前線程也不能擷取寫鎖。
(3)判斷目前線程擷取寫鎖是否超過最大次數,若超過,抛異常,反之更新同步狀态(此時目前線程已擷取寫鎖,更新是線程安全的),傳回true。
(4)如果state為0,此時讀鎖或寫鎖都沒有被擷取,判斷是否需要阻塞(公平和非公平方式實作不同),在非公平政策下總是不會被阻塞,在公平政策下會進行判斷(判斷同步隊列中是否有等待時間更長的線程,若存在,則需要被阻塞,否則,無需阻塞),如果不需要阻塞,則CAS更新同步狀态,若CAS成功則傳回true,失敗則說明鎖被别的線程搶去了,傳回false。如果需要阻塞則也傳回false。
(5)成功擷取寫鎖後,将目前線程設定為占有寫鎖的線程,傳回true。
方法流程圖如下:
2.3.2、寫鎖的釋放,tryRelease方法:
protected final boolean tryRelease(int releases) {
//若鎖的持有者不是目前線程,抛出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//寫鎖的新線程數
int nextc = getState() - releases;
//如果獨占模式重入數為0了,說明獨占模式被釋放
boolean free = exclusiveCount(nextc) == 0;
if (free)
//若寫鎖的新線程數為0,則将鎖的持有者設定為null
setExclusiveOwnerThread(null);
//設定寫鎖的新線程數
//不管獨占模式是否被釋放,更新獨占重入數
setState(nextc);
return free;
}
寫鎖的釋放過程還是相對而言比較簡單的:首先檢視目前線程是否為寫鎖的持有者,如果不是抛出異常。然後檢查釋放後寫鎖的線程數是否為0,如果為0則表示寫鎖空閑了,釋放鎖資源将鎖的持有線程設定為null,否則釋放僅僅隻是一次重入鎖而已,并不能将寫鎖的線程清空。
說明:此方法用于釋放寫鎖資源,首先會判斷該線程是否為獨占線程,若不為獨占線程,則抛出異常,否則,計算釋放資源後的寫鎖的數量,若為0,表示成功釋放,資源不将被占用,否則,表示資源還被占用。其方法流程圖如下。
2.4、讀鎖的擷取與釋放
類似于寫鎖,讀鎖的lock和unlock的實際實作對應Sync的 tryAcquireShared 和 tryReleaseShared方法。
2.4.1、讀鎖的擷取,看下tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
// 擷取目前線程
Thread current = Thread.currentThread();
// 擷取狀态
int c = getState();
//如果寫鎖線程數 != 0 ,且獨占鎖不是目前線程則傳回失敗,因為存在鎖降級
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 讀鎖數量
int r = sharedCount(c);
/*
* readerShouldBlock():讀鎖是否需要等待(公平鎖原則)
* r < MAX_COUNT:持有線程小于最大數(65535)
* compareAndSetState(c, c + SHARED_UNIT):設定讀取鎖狀态
*/
// 讀線程是否應該被阻塞、并且小于最大值、并且比較設定成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
if (r == 0) { // 讀鎖數量為0
// 設定第一個讀線程
firstReader = current;
// 讀線程占用的資源數為1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 目前線程為第一個讀線程,表示第一個讀鎖線程重入
// 占用資源數加1
firstReaderHoldCount++;
} else { // 讀鎖數量不為0并且不為目前線程
// 擷取計數器
HoldCounter rh = cachedHoldCounter;
// 計數器為空或者計數器的tid不為目前正在運作的線程的tid
if (rh == null || rh.tid != getThreadId(current))
// 擷取目前線程對應的計數器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 計數為0
//加入到readHolds中
readHolds.set(rh);
//計數+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
其中sharedCount方法表示占有讀鎖的線程數量,源碼如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
說明:直接将state右移16位,就可以得到讀鎖的線程數量,因為state的高16位表示讀鎖,對應的第十六位表示寫鎖數量。
讀鎖擷取鎖的過程比寫鎖稍微複雜些,首先判斷寫鎖是否為0并且目前線程不占有獨占鎖,直接傳回;否則,判斷讀線程是否需要被阻塞并且讀鎖數量是否小于最大值并且比較設定狀态成功,若目前沒有讀鎖,則設定第一個讀線程firstReader和firstReaderHoldCount;若目前線程線程為第一個讀線程,則增加firstReaderHoldCount;否則,将設定目前線程對應的HoldCounter對象的值。流程圖如下。
注意:更新成功後會在firstReaderHoldCount中或readHolds(ThreadLocal類型的)的本線程副本中記錄目前線程重入數(23行至43行代碼),這是為了實作jdk1.6中加入的getReadHoldCount()方法的,這個方法能擷取目前線程重入共享鎖的次數(state中記錄的是多個線程的總重入次數),加入了這個方法讓代碼複雜了不少,但是其原理還是很簡單的:如果目前隻有一個線程的話,還不需要動用ThreadLocal,直接往firstReaderHoldCount這個成員變量裡存重入數,當有第二個線程來的時候,就要動用ThreadLocal變量readHolds了,每個線程擁有自己的副本,用來儲存自己的重入數。
fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 無限循環
// 擷取狀态
int c = getState();
if (exclusiveCount(c) != 0) { // 寫線程數量不為0
if (getExclusiveOwnerThread() != current) // 不為目前線程
return -1;
} else if (readerShouldBlock()) { // 寫線程數量為0并且讀線程被阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 目前線程為第一個讀線程
// assert firstReaderHoldCount > 0;
} else { // 目前線程不為第一個讀線程
if (rh == null) { // 計數器不為空
//
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) { // 計數器為空或者計數器的tid不為目前正在運作的線程的tid
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) // 讀鎖數量為最大值,抛出異常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 比較并且設定成功
if (sharedCount(c) == 0) { // 讀線程數量為0
// 設定第一個讀線程
firstReader = current;
//
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
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;
}
}
}
View Code
說明:在tryAcquireShared函數中,如果下列三個條件不滿足(讀線程是否應該被阻塞、小于最大值、比較設定成功)則會進行fullTryAcquireShared函數中,它用來保證相關操作可以成功。其邏輯與tryAcquireShared邏輯類似,不再累贅。
2.4.2、讀鎖的釋放,tryReleaseShared方法
protected final boolean tryReleaseShared(int unused) {
// 擷取目前線程
Thread current = Thread.currentThread();
if (firstReader == current) { // 目前線程為第一個讀線程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 讀線程占用的資源數為1
firstReader = null;
else // 減少占用的資源
firstReaderHoldCount--;
} else { // 目前線程不為第一個讀線程
// 擷取緩存的計數器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 計數器為空或者計數器的tid不為目前正在運作的線程的tid
// 擷取目前線程對應的計數器
rh = readHolds.get();
// 擷取計數
int count = rh.count;
if (count <= 1) { // 計數小于等于1
// 移除
readHolds.remove();
if (count <= 0) // 計數小于等于0,抛出異常
throw unmatchedUnlockException();
}
// 減少計數
--rh.count;
}
for (;;) { // 無限循環
// 擷取狀态
int c = getState();
// 擷取狀态
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) // 比較并進行設定
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
說明:此方法表示讀鎖線程釋放鎖。首先判斷目前線程是否為第一個讀線程firstReader,若是,則判斷第一個讀線程占有的資源數firstReaderHoldCount是否為1,若是,則設定第一個讀線程firstReader為空,否則,将第一個讀線程占有的資源數firstReaderHoldCount減1;若目前線程不是第一個讀線程,那麼首先會擷取緩存計數器(上一個讀鎖線程對應的計數器 ),若計數器為空或者tid不等于目前線程的tid值,則擷取目前線程的計數器,如果計數器的計數count小于等于1,則移除目前線程對應的計數器,如果計數器的計數count小于等于0,則抛出異常,之後再減少計數即可。無論何種情況,都會進入無限循環,該循環可以確定成功設定狀态state。其流程圖如下。
在讀鎖的擷取、釋放過程中,總是會有一個對象存在着,同時該對象在擷取線程擷取讀鎖是+1,釋放讀鎖時-1,該對象就是HoldCounter。
2.4.2.1、HoldCounter
要明白HoldCounter就要先明白讀鎖。前面提過讀鎖的内在實作機制就是共享鎖,對于共享鎖其實我們可以稍微的認為它不是一個鎖的概念,它更加像一個計數器的概念。一次共享鎖操作就相當于一次計數器的操作,擷取共享鎖計數器+1,釋放共享鎖計數器-1。隻有當線程擷取共享鎖後才能對共享鎖進行釋放、重入操作。是以HoldCounter的作用就是目前線程持有共享鎖的數量,這個數量必須要與線程綁定在一起,否則操作其他線程鎖就會抛出異常。
if (r == 0) {//r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//第一個讀鎖線程重入
firstReaderHoldCount++;
} else { //非firstReader計數
HoldCounter rh = cachedHoldCounter;//readHoldCounter緩存
//rh == null 或者 rh.tid != current.getId(),需要擷取rh
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh); //加入到readHolds中
rh.count++; //計數+1
}
這裡為什麼要搞一個firstRead、firstReaderHoldCount呢?而不是直接使用else那段代碼?這是為了一個效率問題,firstReader是不會放入到readHolds中的,如果讀鎖僅有一個的情況下就會避免查找readHolds。可能就看這個代碼還不是很了解HoldCounter。我們先看firstReader、firstReaderHoldCount的定義:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
這兩個變量比較簡單,一個表示線程,當然該線程是一個特殊的線程,一個是firstReader的重入計數。
HoldCounter的定義:
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
在HoldCounter中僅有count和tid兩個變量,其中count代表着計數器,tid是線程的id。但是如果要将一個對象和線程綁定起來僅記錄tid肯定不夠的,而且HoldCounter根本不能起到綁定對象的作用,隻是記錄線程tid而已。
誠然,在java中,我們知道如果要将一個線程和對象綁定在一起隻有ThreadLocal才能實作。是以如下:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
ThreadLocalHoldCounter繼承ThreadLocal,并且重寫了initialValue方法。
故而,HoldCounter應該就是綁定線程上的一個計數器,而ThradLocalHoldCounter則是線程綁定的ThreadLocal。從上面我們可以看到ThreadLocal将HoldCounter綁定到目前線程上,同時HoldCounter也持有線程Id,這樣在釋放鎖的時候才能知道ReadWriteLock裡面緩存的上一個讀取線程(cachedHoldCounter)是否是目前線程。這樣做的好處是可以減少ThreadLocal.get()的次數,因為這也是一個耗時操作。需要說明的是這樣HoldCounter綁定線程id而不綁定線程對象的原因是避免HoldCounter和ThreadLocal互相綁定而GC難以釋放它們(盡管GC能夠智能的發現這種引用而回收它們,但是這需要一定的代價),是以其實這樣做隻是為了幫助GC快速回收對象而已。
三、總結
通過上面的源碼分析,我們可以發現一個現象:
線上程持有讀鎖的情況下,該線程不能取得寫鎖(因為擷取寫鎖的時候,如果發現目前的讀鎖被占用,就馬上擷取失敗,不管讀鎖是不是被目前線程持有)。
線上程持有寫鎖的情況下,該線程可以繼續擷取讀鎖(擷取讀鎖時如果發現寫鎖被占用,隻有寫鎖沒有被目前線程占用的情況才會擷取失敗)。
仔細想想,這個設計是合理的:因為當線程擷取讀鎖的時候,可能有其他線程同時也在持有讀鎖,是以不能把擷取讀鎖的線程“更新”為寫鎖;而對于獲得寫鎖的線程,它一定獨占了讀寫鎖,是以可以繼續讓它擷取讀鎖,當它同時擷取了寫鎖和讀鎖後,還可以先釋放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”為了讀鎖。
綜上:
一個線程要想同時持有寫鎖和讀鎖,必須先擷取寫鎖再擷取讀鎖;寫鎖可以“降級”為讀鎖;讀鎖不能“更新”為寫鎖。
參看文章:
https://www.cnblogs.com/xiaoxi/p/9140541.html
https://www.cnblogs.com/yulinfeng/p/6942264.html
https://www.cnblogs.com/zaizhoumo/p/7782941.html
發的
轉載于:https://www.cnblogs.com/bjlhx/p/10601979.html