1、讀寫鎖簡介java
現實中有這樣一種場景:對共享資源有讀和寫的操做,且寫操做沒有讀操做那麼頻繁。在沒有寫操做的時候,多個線程同時讀一個資源沒有任何問題,因此應該容許多個線程同時讀取共享資源;可是若是一個線程想去寫這些共享資源,就不該該容許其餘線程對該資源進行讀和寫的操做了。緩存
針對這種場景,JAVA的併發包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操做相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖,描述以下:安全
線程進入讀鎖的前提條件:併發
沒有其餘線程的寫鎖,函數
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。源碼分析
線程進入寫鎖的前提條件:ui
沒有其餘線程的讀鎖this
沒有其餘線程的寫鎖spa
而讀寫鎖有如下三個重要的特性:線程
(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量仍是非公平優於公平。
(2)重進入:讀鎖和寫鎖都支持線程重進入。
(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖可以降級成爲讀鎖。
2、源碼解讀
咱們先來看下 ReentrantReadWriteLock 類的總體結構:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { /** 讀鎖 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 寫鎖 */ private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; /** 使用默認(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock() { this(false); } /** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */ 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; } abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync {} static final class FairSync extends Sync {} public static class ReadLock implements Lock, java.io.Serializable {} public static class WriteLock implements Lock, java.io.Serializable {} }
一、類的繼承關係
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}
說明:能夠看到,ReentrantReadWriteLock實現了ReadWriteLock接口,ReadWriteLock接口定義了獲取讀鎖和寫鎖的規範,具體須要實現類去實現;同時其還實現了Serializable接口,表示能夠進行序列化,在源代碼中能夠看到ReentrantReadWriteLock實現了本身的序列化邏輯。
二、類的內部類
ReentrantReadWriteLock有五個內部類,五個內部類之間也是相互關聯的。內部類的關係以下圖所示。
說明:如上圖所示,Sync繼承自AQS、NonfairSync繼承自Sync類、FairSync繼承自Sync類(經過構造函數傳入的布爾值決定要構造哪種Sync實例);ReadLock實現了Lock接口、WriteLock也實現了Lock接口。
Sync類:
(1)類的繼承關係
abstract static class Sync extends AbstractQueuedSynchronizer {}
說明:Sync抽象類繼承自AQS抽象類,Sync類提供了對ReentrantReadWriteLock的支持。
(2)類的內部類
Sync類內部存在兩個內部類,分別爲HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要與讀鎖配套使用,其中,HoldCounter源碼以下。
// 計數器 static final class HoldCounter { // 計數 int count = 0; // Use id, not reference, to avoid garbage retention // 獲取當前線程的TID屬性的值 final long tid = getThreadId(Thread.currentThread()); }
說明:HoldCounter主要有兩個屬性,count和tid,其中count表示某個讀線程重入的次數,tid表示該線程的tid字段的值,該字段能夠用來惟一標識一個線程。ThreadLocalHoldCounter的源碼以下
// 本地線程計數器 static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { // 重寫初始化方法,在沒有進行set的狀況下,獲取的都是該HoldCounter值 public HoldCounter initialValue() { return new HoldCounter(); } }
說明:ThreadLocalHoldCounter重寫了ThreadLocal的initialValue方法,ThreadLocal類能夠將線程與對象相關聯。在沒有進行set的狀況下,get到的均是initialValue方法裏面生成的那個HolderCounter對象。
(3)類的屬性
abstract static class Sync extends AbstractQueuedSynchronizer { // 版本序列號 private static final long serialVersionUID = 6317671515068378041L; // 高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; // 本地線程計數器 private transient ThreadLocalHoldCounter readHolds; // 緩存的計數器 private transient HoldCounter cachedHoldCounter; // 第一個讀線程 private transient Thread firstReader = null; // 第一個讀線程的計數 private transient int firstReaderHoldCount; }
說明:該屬性中包括了讀鎖、寫鎖線程的最大量。本地線程計數器等。
(4)類的構造函數
// 構造函數 Sync() { // 本地線程計數器 readHolds = new ThreadLocalHoldCounter(); // 設置AQS的狀態 setState(getState()); // ensures visibility of readHolds }
說明:在Sync的構造函數中設置了本地線程計數器和AQS的狀態state。
三、讀寫狀態的設計
同步狀態在重入鎖的實現中是表示被同一個線程重複獲取的次數,即一個整形變量來維護,可是以前的那個表示僅僅表示是否鎖定,而不用區分是讀鎖仍是寫鎖。而讀寫鎖須要在同步狀態(一個整形變量)上維護多個讀線程和一個寫線程的狀態。
讀寫鎖對於同步狀態的實現是在一個整形變量上經過「按位切割使用」:將變量切割成兩部分,高16位表示讀,低16位表示寫。
假設當前同步狀態值爲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,則表示該讀寫鎖的讀鎖已被獲取。
四、寫鎖的獲取與釋放
看下WriteLock類中的lock和unlock方法:
public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); }
能夠看到就是調用的獨佔式同步狀態的獲取與釋放,所以真實的實現就是Sync的 tryAcquire和 tryRelease。
寫鎖的獲取,看下tryAcquire:
1 protected final boolean tryAcquire(int acquires) { 2 //當前線程 3 Thread current = Thread.currentThread(); 4 //獲取狀態 5 int c = getState(); 6 //寫線程數量(即獲取獨佔鎖的重入數) 7 int w = exclusiveCount(c); 8 9 //當前同步狀態state != 0,說明已經有其餘線程獲取了讀鎖或寫鎖 10 if (c != 0) { 11 // 當前state不爲0,此時:若是寫鎖狀態爲0說明讀鎖此時被佔用返回false; 12 // 若是寫鎖狀態不爲0且寫鎖沒有被當前線程持有返回false 13 if (w == 0 || current != getExclusiveOwnerThread()) 14 return false; 15 16 //判斷同一線程獲取寫鎖是否超過最大次數(65535),支持可重入 17 if (w + exclusiveCount(acquires) > MAX_COUNT) 18 throw new Error("Maximum lock count exceeded"); 19 //更新狀態 20 //此時當前線程已持有寫鎖,如今是重入,因此只須要修改鎖的數量便可。 21 setState(c + acquires); 22 return true; 23 } 24 25 //到這裏說明此時c=0,讀鎖和寫鎖都沒有被獲取 26 //writerShouldBlock表示是否阻塞 27 if (writerShouldBlock() || 28 !compareAndSetState(c, c + acquires)) 29 return false; 30 31 //設置鎖爲當前線程全部 32 setExclusiveOwnerThread(current); 33 return true; 34 }
其中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。
方法流程圖以下:
寫鎖的釋放,tryRelease方法:
1 protected final boolean tryRelease(int releases) { 2 //若鎖的持有者不是當前線程,拋出異常 3 if (!isHeldExclusively()) 4 throw new IllegalMonitorStateException(); 5 //寫鎖的新線程數 6 int nextc = getState() - releases; 7 //若是獨佔模式重入數爲0了,說明獨佔模式被釋放 8 boolean free = exclusiveCount(nextc) == 0; 9 if (free) 10 //若寫鎖的新線程數爲0,則將鎖的持有者設置爲null 11 setExclusiveOwnerThread(null); 12 //設置寫鎖的新線程數 13 //無論獨佔模式是否被釋放,更新獨佔重入數 14 setState(nextc); 15 return free; 16 }
寫鎖的釋放過程仍是相對而言比較簡單的:首先查看當前線程是否爲寫鎖的持有者,若是不是拋出異常。而後檢查釋放後寫鎖的線程數是否爲0,若是爲0則表示寫鎖空閒了,釋放鎖資源將鎖的持有線程設置爲null,不然釋放僅僅只是一次重入鎖而已,並不能將寫鎖的線程清空。
說明:此方法用於釋放寫鎖資源,首先會判斷該線程是否爲獨佔線程,若不爲獨佔線程,則拋出異常,不然,計算釋放資源後的寫鎖的數量,若爲0,表示成功釋放,資源不將被佔用,不然,表示資源還被佔用。其方法流程圖以下。
五、讀鎖的獲取與釋放
相似於寫鎖,讀鎖的lock和unlock的實際實現對應Sync的 tryAcquireShared 和 tryReleaseShared方法。
讀鎖的獲取,看下tryAcquireShared方法
1 protected final int tryAcquireShared(int unused) { 2 // 獲取當前線程 3 Thread current = Thread.currentThread(); 4 // 獲取狀態 5 int c = getState(); 6 7 //若是寫鎖線程數 != 0 ,且獨佔鎖不是當前線程則返回失敗,由於存在鎖降級 8 if (exclusiveCount(c) != 0 && 9 getExclusiveOwnerThread() != current) 10 return -1; 11 // 讀鎖數量 12 int r = sharedCount(c); 13 /* 14 * readerShouldBlock():讀鎖是否須要等待(公平鎖原則) 15 * r < MAX_COUNT:持有線程小於最大數(65535) 16 * compareAndSetState(c, c + SHARED_UNIT):設置讀取鎖狀態 17 */ 18 // 讀線程是否應該被阻塞、而且小於最大值、而且比較設置成功 19 if (!readerShouldBlock() && 20 r < MAX_COUNT && 21 compareAndSetState(c, c + SHARED_UNIT)) { 22 //r == 0,表示第一個讀鎖線程,第一個讀鎖firstRead是不會加入到readHolds中 23 if (r == 0) { // 讀鎖數量爲0 24 // 設置第一個讀線程 25 firstReader = current; 26 // 讀線程佔用的資源數爲1 27 firstReaderHoldCount = 1; 28 } else if (firstReader == current) { // 當前線程爲第一個讀線程,表示第一個讀鎖線程重入 29 // 佔用資源數加1 30 firstReaderHoldCount++; 31 } else { // 讀鎖數量不爲0而且不爲當前線程 32 // 獲取計數器 33 HoldCounter rh = cachedHoldCounter; 34 // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid 35 if (rh == null || rh.tid != getThreadId(current)) 36 // 獲取當前線程對應的計數器 37 cachedHoldCounter = rh = readHolds.get(); 38 else if (rh.count == 0) // 計數爲0 39 //加入到readHolds中 40 readHolds.set(rh); 41 //計數+1 42 rh.count++; 43 } 44 return 1; 45 } 46 return fullTryAcquireShared(current); 47 }
其中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; } } }
說明:在tryAcquireShared函數中,若是下列三個條件不知足(讀線程是否應該被阻塞、小於最大值、比較設置成功)則會進行fullTryAcquireShared函數中,它用來保證相關操做能夠成功。其邏輯與tryAcquireShared邏輯相似,再也不累贅。
讀鎖的釋放,tryReleaseShared方法
1 protected final boolean tryReleaseShared(int unused) { 2 // 獲取當前線程 3 Thread current = Thread.currentThread(); 4 if (firstReader == current) { // 當前線程爲第一個讀線程 5 // assert firstReaderHoldCount > 0; 6 if (firstReaderHoldCount == 1) // 讀線程佔用的資源數爲1 7 firstReader = null; 8 else // 減小佔用的資源 9 firstReaderHoldCount--; 10 } else { // 當前線程不爲第一個讀線程 11 // 獲取緩存的計數器 12 HoldCounter rh = cachedHoldCounter; 13 if (rh == null || rh.tid != getThreadId(current)) // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid 14 // 獲取當前線程對應的計數器 15 rh = readHolds.get(); 16 // 獲取計數 17 int count = rh.count; 18 if (count <= 1) { // 計數小於等於1 19 // 移除 20 readHolds.remove(); 21 if (count <= 0) // 計數小於等於0,拋出異常 22 throw unmatchedUnlockException(); 23 } 24 // 減小計數 25 --rh.count; 26 } 27 for (;;) { // 無限循環 28 // 獲取狀態 29 int c = getState(); 30 // 獲取狀態 31 int nextc = c - SHARED_UNIT; 32 if (compareAndSetState(c, nextc)) // 比較並進行設置 33 // Releasing the read lock has no effect on readers, 34 // but it may allow waiting writers to proceed if 35 // both read and write locks are now free. 36 return nextc == 0; 37 } 38 }
說明:此方法表示讀鎖線程釋放鎖。首先判斷當前線程是否爲第一個讀線程firstReader,如果,則判斷第一個讀線程佔有的資源數firstReaderHoldCount是否爲1,如果,則設置第一個讀線程firstReader爲空,不然,將第一個讀線程佔有的資源數firstReaderHoldCount減1;若當前線程不是第一個讀線程,那麼首先會獲取緩存計數器(上一個讀鎖線程對應的計數器 ),若計數器爲空或者tid不等於當前線程的tid值,則獲取當前線程的計數器,若是計數器的計數count小於等於1,則移除當前線程對應的計數器,若是計數器的計數count小於等於0,則拋出異常,以後再減小計數便可。不管何種狀況,都會進入無限循環,該循環能夠確保成功設置狀態state。其流程圖以下。
在讀鎖的獲取、釋放過程當中,老是會有一個對象存在着,同時該對象在獲取線程獲取讀鎖是+1,釋放讀鎖時-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快速回收對象而已。
3、總結
經過上面的源碼分析,咱們能夠發現一個現象:
在線程持有讀鎖的狀況下,該線程不能取得寫鎖(由於獲取寫鎖的時候,若是發現當前的讀鎖被佔用,就立刻獲取失敗,無論讀鎖是否是被當前線程持有)。
在線程持有寫鎖的狀況下,該線程能夠繼續獲取讀鎖(獲取讀鎖時若是發現寫鎖被佔用,只有寫鎖沒有被當前線程佔用的狀況纔會獲取失敗)。
仔細想一想,這個設計是合理的:由於當線程獲取讀鎖的時候,可能有其餘線程同時也在持有讀鎖,所以不能把獲取讀鎖的線程「升級」爲寫鎖;而對於得到寫鎖的線程,它必定獨佔了讀寫鎖,所以能夠繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖後,還能夠先釋放寫鎖繼續持有讀鎖,這樣一個寫鎖就「降級」爲了讀鎖。
綜上:
一個線程要想同時持有寫鎖和讀鎖,必須先獲取寫鎖再獲取讀鎖;寫鎖能夠「降級」爲讀鎖;讀鎖不能「升級」爲寫鎖。