ReentrantReadWriteLock?讀寫鎖分析總結(jié)
一、讀寫鎖簡介
現(xiàn)實(shí)中有這樣一種場景:對(duì)共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁(讀多寫少)。在沒有寫操作的時(shí)候,多個(gè)線程同時(shí)讀一個(gè)資源沒有任何問題,所以應(yīng)該允許多個(gè)線程同時(shí)讀取共享資源(讀讀可以并發(fā));但是如果一個(gè)線程想去寫這些共享資源,就不應(yīng)該允許其他線程對(duì)該資源進(jìn)行讀和寫操作了(讀寫,寫讀,寫寫互斥)。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。
針對(duì)這種場景,JAVA的并發(fā)包提供了讀寫鎖 ReentrantReadWriteLock,它內(nèi)部,維護(hù)了一對(duì)相關(guān)的鎖,一個(gè)用于只讀操作,稱為讀鎖;一個(gè)用于寫入操作,稱為寫鎖,描述如下:線程進(jìn)入讀鎖的前提條件:
- 沒有其他線程的寫鎖
- 沒有寫請(qǐng)求或者有寫請(qǐng)求,但調(diào)用線程和持有鎖的線程是同一個(gè)。
線程進(jìn)入寫鎖的前提條件:
- 沒有其他線程的讀鎖
- 沒有其他線程的寫鎖
而讀寫鎖有以下三個(gè)重要的特性:
- 公平選擇性:支持非公平(默認(rèn))和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平。
- 可重入:讀鎖和寫鎖都支持線程重入。以讀寫線程為例:讀線程獲取讀鎖后,能夠再次獲取讀鎖。寫線程在獲取寫鎖之后能夠再次獲取寫鎖,同時(shí)也可以獲取讀鎖。
- 鎖降級(jí):遵循獲取寫鎖、再獲取讀鎖最后釋放寫鎖的次序,寫鎖能夠降級(jí)成為讀鎖。
看了上面的描述大家可能有點(diǎn)暈,我就舉一個(gè)之前開發(fā)訂單的例子,輔助大家理解。 我們的訂單有一個(gè)主單和子單的概念:主單編碼為 orderCode
, 子單編碼為 subOrderCode
對(duì)應(yīng)關(guān)系是 1:N。 我在退款的時(shí)候,需要支持子單,主單退款。 子單退款,的維度是 subOrderCode
主單退款,的維度是 orderCode
可能出現(xiàn)并發(fā)的情況,我們可以對(duì) orderCode
加一把讀寫鎖
- 如果是主單退款的情況,是不是子單退款就是互斥的
- 如果是子單退款的情況,其實(shí)就可以并行的,但是子單是
subOrderCode
維度,還需要加一個(gè)subOrderCode
的互斥鎖。
二、讀寫鎖使用
如何同時(shí)存儲(chǔ)讀寫鎖,可以通過 state 的值進(jìn)行存儲(chǔ),高 16 位表示讀鎖,低 16 位表示寫鎖。 比如: 0000 0000 0000 0000 (1<<16) 0000 0000 0000 0000 高 16 位不為0: 有讀鎖 c >>>16 低 16 位不為0: 有寫鎖 5
ReadWriteLock 接口
我們可以看到 ReentranReadWriteLock 有兩把鎖,一把讀鎖,一把寫鎖。
使用例子
緩存操作:
public class ReentrantReadWriteLockCacheTest { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 獲取一個(gè)key對(duì)應(yīng)的value public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } // 設(shè)置key對(duì)應(yīng)的value,并返回舊的value public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } // 清空所有的內(nèi)容 public static final void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } }
上述示例中,Cache組合一個(gè)非線程安全的HashMap作為緩存的實(shí)現(xiàn),同時(shí)使用讀寫鎖的 讀鎖和寫鎖來保證Cache是線程安全的。在讀操作get(String key)方法中,需要獲取讀鎖,這 使得并發(fā)訪問該方法時(shí)不會(huì)被阻塞。寫操作put(String key,Object value)方法和clear()方法, 在更新 HashMap時(shí)必須提前獲取寫鎖,當(dāng)獲取寫鎖后,其他線程對(duì)于讀鎖和寫鎖的獲取均被 阻塞,而 只有寫鎖被釋放之后,其他讀寫操作才能繼續(xù)。Cache使用讀寫鎖提升讀操作的并發(fā) 性,也保證每次寫操作對(duì)所有的讀寫操作的可見性,同時(shí)簡化了編程方式。
三、鎖的降級(jí)
鎖降級(jí)指的是寫鎖降級(jí)成為讀鎖。如果當(dāng)前線程擁有寫鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級(jí)。鎖降級(jí)是指把持住(當(dāng)前擁有的)寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。鎖降級(jí)可以幫助我們拿到當(dāng)前線程修改后的結(jié)果而不被其他線程所破壞,防止更新丟失。
鎖降級(jí)的使用示例
因?yàn)閿?shù)據(jù)不常變化,所以多個(gè)線程可以并發(fā)地進(jìn)行數(shù)據(jù)處理,當(dāng)數(shù)據(jù)變更后,如果當(dāng)前線程感知到數(shù)據(jù)變化,則進(jìn)行數(shù)據(jù)的準(zhǔn)備工作,同時(shí)其他處理線程被阻塞,直到當(dāng)前線程完成數(shù)據(jù)的準(zhǔn)備工作。
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock readLock = rwl.readLock(); private final Lock writeLock = rwl.writeLock(); private volatile boolean update = false; public void processData() { readLock.lock(); if (!update) { // 必須先釋放讀鎖 readLock.unlock(); // 鎖降級(jí)從寫鎖獲取到開始 writeLock.lock(); try { if (!update) { // TODO 準(zhǔn)備數(shù)據(jù)的流程(略) update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 鎖降級(jí)完成,寫鎖降級(jí)為讀鎖 } try { //TODO 使用數(shù)據(jù)的流程(略) } finally { readLock.unlock(); } }
注意事項(xiàng):
- 讀鎖不支持條件變量
- 重入時(shí)不升級(jí)不支持:持有讀鎖的情況下,去獲取寫鎖,會(huì)導(dǎo)致永久等待
- 重入時(shí)支持降級(jí):持有寫鎖的情況下可以去獲取讀鎖
四、ReentranReadWriteLock 結(jié)構(gòu)
方法結(jié)構(gòu)設(shè)計(jì)
讀寫狀態(tài)設(shè)計(jì)
五、源碼分析
寫鎖的加鎖
方法 tryAcquire
是寫鎖的加鎖核心邏輯
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(); // 獲取寫鎖狀態(tài) int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire // 重入 setState(c + acquires); return true; } // 獲取寫鎖 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 設(shè)置寫鎖 owner setExclusiveOwnerThread(current); return 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; }
讀鎖的獲取
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); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 首次獲取讀鎖 if (r == 0) { firstReader = current; // 第一個(gè)線程重入 firstReaderHoldCount = 1; } else if (firstReader == current) { // 重入 firstReaderHoldCount++; } else { // 后續(xù)線程,通過 ThreadLocal 獲取重入次數(shù) HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }
fullTryAcquireShared
方法如下:
final int fullTryAcquireShared(Thread current) { /* * This code is in part redundant with that in * tryAcquireShared but is simpler overall by not * complicating tryAcquireShared with interactions between * retries and lazily reading hold counts. */ HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // else we hold the exclusive lock; blocking here // would cause deadlock. } else if (readerShouldBlock()) { // 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)) { 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) { 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; } } }
讀鎖的釋放
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 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; } }
到此這篇關(guān)于ReentrantReadWriteLock 讀寫鎖分析總結(jié)的文章就介紹到這了,更多相關(guān)ReentrantReadWriteLock 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 8中map()和flatMap()方法區(qū)別詳解
這篇文章主要為大家介紹了Java 8中map()和flatMap()方法區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07springboot熱部署知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理了關(guān)于springboot熱部署的知識(shí)點(diǎn)內(nèi)容,有興趣的朋友們參考學(xué)習(xí)下。2019-06-06詳解SpringBoot項(xiàng)目整合Vue做一個(gè)完整的用戶注冊(cè)功能
本文主要介紹了SpringBoot項(xiàng)目整合Vue做一個(gè)完整的用戶注冊(cè)功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07