本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫
本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。
通過以下幾部分來分析Java提供的讀寫鎖ReentrantReadWriteLock:
為什麼需要讀寫鎖讀寫鎖的使用DemoReentrantReadWriteLock類結構記錄讀寫鎖狀態源碼分析讀鎖的獲取與釋放源碼分析寫鎖的獲取與釋放鎖降級讀寫鎖應用本文涉及到上下文聯繫較多,經常需要上下滑動查看,篇幅太多很不方便,而且文章太長閱讀體驗也不好,所以分成讀寫鎖(上)和讀寫鎖(下)兩篇。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。
7. 寫鎖獲取
rwl.writeLock().lock()
的調用
public void lock() {sync.acquire(1);}public final void acquire(int arg) { if (!tryAcquire(arg) && // 寫鎖實現了獲取鎖的方法,下文詳細講解 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 獲取鎖失敗進入同步隊列,等待被喚醒,AQS一文中重點講過 selfInterrupt();}
先分析一下可以獲取寫鎖的條件:
當前鎖的狀態1)沒有線程佔用鎖(讀寫鎖都沒被佔用) 2)線程佔用寫鎖時,線程再次來獲取寫鎖,也就是重入AQS隊列中的情況,如果是公平鎖,同步隊列中有線程等鎖時,當前線程是不可以先獲取鎖的,必須到隊列中排隊。寫鎖的標誌位只有16位,最多重入2^16-1次。
/*** ReentrantReadWriteLock.Sync.tryAcquire(int) */protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c);// 寫鎖標誌位 // 進到這個if裡,c!=0表示有線程佔用鎖 // 當有線程佔用鎖時,只有一種情況是可以獲取寫鎖的,那就是寫鎖重入 if (c != 0) { /* * 兩種情況返回false * 1.(c != 0 & w == 0) * c!=0表示標誌位!=0,w==0表示寫鎖標誌位==0,總的標誌位不為0而寫鎖標誌位(低16位)為0,只能是讀鎖標誌位(高16位)不為0 * 也就是有線程佔用讀鎖,此時不能獲取寫鎖,返回false * * 2.(c != 0 & w != 0 & current != getExclusiveOwnerThread()) * c != 0 & w != 0 表示寫鎖標誌位不為0,有線程佔用寫鎖 * current != getExclusiveOwnerThread() 佔用寫鎖的線程不是當前線程 * 不能獲取寫鎖,返回false */ if (w == 0 || current != getExclusiveOwnerThread()) return false; // 重入次數不能超過2^16-1 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); /* * 修改標誌位 * 這裡修改標誌位為什麼沒有用CAS原子操作呢? * 因為到這裡肯定是寫鎖重入了,寫鎖是獨佔鎖,不會有其他線程來搗亂。 */ setState(c + acquires); return true; } /* * 到這裡表示鎖是沒有被線程佔用的,因為鎖被線程佔用的情況在上個if裡處理並返回了 * 所以這裡直接檢查AQS隊列情況,沒問題的話CAS修改標誌位獲取鎖 */ if (writerShouldBlock() || // 檢查AQS隊列中的情況,看是當前線程是否可以獲取寫鎖 !compareAndSetState(c, c + acquires)) // 修改寫鎖標誌位 return false; setExclusiveOwnerThread(current);// 獲取寫鎖成功,將AQS.exclusiveOwnerThread置為當前線程 return true;}
簡單看下writerShouldBlock()
writerShouldBlock():檢查AQS隊列中的情況,看是當前線程是否可以獲取寫鎖,返回false表示可以獲取寫鎖。
對於公平鎖來說,如果隊列中還有線程在等鎖,就不允許新來的線程獲得鎖,必須進入隊列排隊。
hasQueuedPredecessors()方法在重入鎖的文章中分析過,判斷同步隊列中是否還有等鎖的線程,如果有其他線程等鎖,返回true當前線程不能獲取讀鎖。
// 公平鎖final boolean writerShouldBlock() {return hasQueuedPredecessors();}
對於非公平鎖來說,不需要關心隊列中的情況,有機會直接嘗試搶鎖就好了,所以直接返回false。
// 非公平鎖final boolean writerShouldBlock() {return false;}
8. 寫鎖釋放
寫鎖釋放比較簡單,跟之前的重入鎖釋放基本類似,看下源碼:
public void unlock() {sync.release(1);}/** * 釋放寫鎖,如果釋放之後沒有線程佔用寫鎖,喚醒隊列中的線程來獲取鎖 */public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);// 喚醒head的後繼節點去獲取鎖 return true; } return false;}/** * 釋放寫鎖,修改寫鎖標誌位和exclusiveOwnerThread * 如果這個寫鎖釋放之後,沒有線程佔用寫鎖了,返回true */protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free;}
9. 鎖降級
讀寫鎖支持鎖降級。鎖降級就是寫鎖是可以降級為讀鎖的,但是需要遵循獲取寫鎖、獲取讀鎖、釋放寫鎖的次序。
為什麼要支持鎖降級?
支持降級鎖的情況:線程A持有寫鎖時,線程A要讀取共享數據,線程A直接獲取讀鎖讀取數據就好了。
如果不支持鎖降級會怎麼樣?
線程A持有寫鎖時,線程A要讀取共享數據,但是線程A不能獲取讀鎖,只能等待釋放寫鎖。
當線程A釋放寫鎖之後,線程A獲取讀鎖要和其他線程搶鎖,如果另一個線程B搶到了寫鎖,對數據進行了修改,那麼線程B釋放寫鎖之後,線程A才能獲取讀鎖。線程B獲取到讀鎖之後讀取的數據就不是線程A修改的數據了,也就是髒數據。
源碼中哪裡支持鎖降級?
tryAcquireShared()方法中,當前線程佔用寫鎖時是可以獲取讀鎖的,如下:
protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread(); int c = getState(); /* * 根據鎖的狀態判斷可以獲取讀鎖的情況: * 1. 讀鎖寫鎖都沒有被佔用 * 2. 只有讀鎖被佔用 * 3. 寫鎖被自己線程佔用 * 總結一下,只有在其它線程持有寫鎖時,不能獲取讀鎖,其它情況都可以去獲取。 */ if (exclusiveCount(c) != 0 && // 寫鎖被佔用 getExclusiveOwnerThread() != current) // 持有寫鎖的不是當前線程 return -1; ...
不支持鎖升級
持有寫鎖的線程,去獲取讀鎖的過程稱為鎖降級;持有讀鎖的線程,在沒釋放的情況下不能去獲取寫鎖的過程稱為鎖升級。
讀寫鎖是不支持鎖升級的。獲取寫鎖的tryAcquire()方法:
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); /* * (c != 0 & w == 0)時返回false,不能獲取寫鎖 * c != 0 表示state不是0 * w == 0 表示寫鎖標誌位state的低16位為0 * 所以state的高16位不為0,也就是有線程佔有讀鎖 * 也就是說只要有線程佔有讀鎖返回false,不能獲取寫鎖,當然線程自己持有讀鎖時也就不能獲取寫鎖了 */ if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; ...
8. 應用
讀寫鎖多用於解決讀多寫少的問題,最典型的就是緩存問題。如下是官方給出的應用示例:
class CachedData {Object data; volatile boolean cacheValid; // 讀寫鎖實例 final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { // 獲取讀鎖 rwl.readLock().lock(); if (!cacheValid) { // 如果緩存過期了,或者為 null // 釋放掉讀鎖,然後獲取寫鎖 (後面會看到,沒釋放掉讀鎖就獲取寫鎖,會發生死鎖情況) rwl.readLock().unlock(); rwl.writeLock().lock(); try { if (!cacheValid) { // 重新判斷,因為在等待寫鎖的過程中,可能前面有其他寫線程執行過了 data = ... cacheValid = true; } // 獲取讀鎖 (持有寫鎖的情況下,是允許獲取讀鎖的,稱為 「鎖降級」,反之不行。) rwl.readLock().lock(); } finally { // 釋放寫鎖,此時還剩一個讀鎖 rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { // 釋放讀鎖 rwl.readLock().unlock(); } }}
總結
可以獲取寫鎖的情況只有兩種:
讀鎖和寫鎖都沒有線程佔用當前線程佔用寫鎖,也就寫鎖重入讀寫鎖支持鎖降級,不支持鎖升級。鎖降級就是寫鎖是可以降級為讀鎖的,但是需要遵循獲取寫鎖、獲取讀鎖、釋放寫鎖的次序。
讀寫鎖多用於解決讀多寫少的問題,最典型的就是緩存問題。