Java AQS中ReentrantReadWriteLock讀寫(xiě)鎖的使用
一. 簡(jiǎn)介
為什么會(huì)使用讀寫(xiě)鎖?
日常大多數(shù)見(jiàn)到的對(duì)共享資源有讀和寫(xiě)的操作,寫(xiě)操作并沒(méi)有讀操作那么頻繁(讀多寫(xiě)少),在沒(méi)有寫(xiě)操作的時(shí)候,多個(gè)線程同時(shí)讀一個(gè)資源沒(méi)有任何問(wèn)題,所以應(yīng)該允許多個(gè)線程同時(shí)讀取共享資源(讀讀可以并發(fā));但是如果一個(gè)線程想去寫(xiě)這些共享資源,就不應(yīng)該允許其他線程對(duì)該資源進(jìn)行讀和寫(xiě)操作了(讀寫(xiě),寫(xiě)讀,寫(xiě)寫(xiě)互斥)。在讀多于寫(xiě)的情況下,讀寫(xiě)鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。
針對(duì)這種場(chǎng)景JAVA的并發(fā)包提供了讀寫(xiě)鎖ReentrantReadWriteLock,它內(nèi)部維護(hù)了一對(duì)相關(guān)的鎖,一個(gè)用于只讀操作,稱(chēng)為讀鎖;一個(gè)用于寫(xiě)入操作,稱(chēng)為寫(xiě)鎖。
線程進(jìn)入讀鎖的前提條件:
- 沒(méi)有其他線程的寫(xiě)鎖
- 沒(méi)有寫(xiě)請(qǐng)求或者有寫(xiě)請(qǐng)求,但調(diào)用線程和持有鎖的線程是同一個(gè)。
線程進(jìn)入寫(xiě)鎖的前提條件:
- 沒(méi)有其他線程的讀鎖
- 沒(méi)有其他線程的寫(xiě)鎖
而讀寫(xiě)鎖有以下三個(gè)重要的特性:
- 公平選擇性:支持非公平(默認(rèn))和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平。
- 可重入:讀鎖和寫(xiě)鎖都支持線程重入。以讀寫(xiě)線程為例:讀線程獲取讀鎖后,能夠再次獲取讀鎖。寫(xiě)線程在獲取寫(xiě)鎖之后能夠再次獲取寫(xiě)鎖,同時(shí)也可以獲取讀鎖。
- 鎖降級(jí):遵循獲取寫(xiě)鎖、再獲取讀鎖最后釋放寫(xiě)鎖的次序,寫(xiě)鎖能夠降級(jí)成為讀鎖。
二. 接口及實(shí)現(xiàn)類(lèi)
接口
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
實(shí)現(xiàn)類(lèi)
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; /** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock; /** Inner class providing writelock */ private final ReentrantReadWriteLock.WriteLock writerLock; /** Performs all synchronization mechanics */ 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; }
三.使用
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private Lock r = readWriteLock.readLock(); private Lock w = readWriteLock.writeLock(); // 讀操作上讀鎖 public Data get(String key) { r.lock(); try { // TODO 業(yè)務(wù)邏輯 }finally { r.unlock(); } } // 寫(xiě)操作上寫(xiě)鎖 public Data put(String key, Data value) { w.lock(); try { // TODO 業(yè)務(wù)邏輯 }finally { w.unlock(); }
四. 應(yīng)用場(chǎng)景
ReentrantReadWriteLock適合讀多寫(xiě)少的場(chǎng)景。
public class Cache { 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í)使用讀寫(xiě)鎖的讀鎖和寫(xiě)鎖來(lái)保證Cache是線程安全的。在讀操作get(String key)方法中,需要獲取讀鎖,這使得并發(fā)訪問(wèn)該方法時(shí)不會(huì)被阻塞。寫(xiě)操作put(String key,Object value)方法和clear()方法,在更新 HashMap時(shí)必須提前獲取寫(xiě)鎖,當(dāng)獲取寫(xiě)鎖后,其他線程對(duì)于讀鎖和寫(xiě)鎖的獲取均被阻塞,而只有寫(xiě)鎖被釋放之后,其他讀寫(xiě)操作才能繼續(xù)。Cache使用讀寫(xiě)鎖提升讀操作的并發(fā)性,也保證每次寫(xiě)操作對(duì)所有的讀寫(xiě)操作的可見(jiàn)性,同時(shí)簡(jiǎn)化了編程方式
五. 鎖降級(jí)
鎖降級(jí)指的是寫(xiě)鎖降級(jí)成為讀鎖。如果當(dāng)前線程擁有寫(xiě)鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過(guò)程不能稱(chēng)之為鎖降級(jí)。鎖降級(jí)是指把持?。ó?dāng)前擁有的)寫(xiě)鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫(xiě)鎖的過(guò)程。鎖降級(jí)可以幫助我們拿到當(dāng)前線程修改后的結(jié)果而不被其他線程所破壞,防止更新丟失。
示例
因?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 r = rwl.readLock(); private final Lock w = rwl.writeLock(); private volatile boolean update = false; public void processData() { readLock.lock(); if (!update) { // 必須先釋放讀鎖 readLock.unlock(); // 鎖降級(jí)從寫(xiě)鎖獲取到開(kāi)始 writeLock.lock(); try { if (!update) { // TODO 準(zhǔn)備數(shù)據(jù)的流程(略) update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 鎖降級(jí)完成,寫(xiě)鎖降級(jí)為讀鎖 } try { //TODO 使用數(shù)據(jù)的流程(略) } finally { readLock.unlock(); } }
鎖降級(jí)中讀鎖的獲取是否必要呢?答案是必要的。主要是為了保證數(shù)據(jù)的可見(jiàn)性,如果當(dāng)前線程不獲取讀鎖而是直接釋放寫(xiě)鎖,假設(shè)此刻另一個(gè)線程(記作線程T)獲取了寫(xiě)鎖并修改了數(shù)據(jù),那么當(dāng)前線程無(wú)法感知線程T的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級(jí)的步驟,則線程T將會(huì)被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫(xiě)鎖進(jìn)行數(shù)據(jù)更新。
RentrantReadWriteLock不支持鎖升級(jí)(把持讀鎖、獲取寫(xiě)鎖,最后釋放讀鎖的過(guò)程)。目的也是保證數(shù)據(jù)可見(jiàn)性,如果讀鎖已被多個(gè)線程獲取,其中任意線程成功獲取了寫(xiě)鎖并更新了數(shù)據(jù),則其更新對(duì)其他獲取到讀鎖的線程是不可見(jiàn)的。
六.源碼解析
在 ReentrantLock 中,使用 Sync 的 int 類(lèi)型的 state 來(lái)表示同步狀態(tài),表示鎖被一個(gè)線程重復(fù)獲取的次數(shù)。但是,讀寫(xiě)鎖 ReentrantReadWriteLock 內(nèi)部維護(hù)著一對(duì)讀寫(xiě)鎖,如果要用一個(gè)變量維護(hù)多種狀態(tài),需要采用“按位切割使用”的方式來(lái)維護(hù)這個(gè)變量,將其切分為兩部分:高16為表示讀,低16為表示寫(xiě)。
分割之后,讀寫(xiě)鎖是如何迅速確定讀鎖和寫(xiě)鎖的狀態(tài)呢?通過(guò)位運(yùn)算。假如當(dāng)前同步狀態(tài)為S,那么:
- 寫(xiě)狀態(tài),等于 S & 0x0000FFFF(將高 16 位全部抹去)。 當(dāng)寫(xiě)狀態(tài)加1,等于S+1.
- 讀狀態(tài),等于 S >>> 16 (無(wú)符號(hào)補(bǔ) 0 右移 16 位)。當(dāng)讀狀態(tài)加1,等于S+(1<<16),也就是S+0x00010000
根據(jù)狀態(tài)的劃分能得出一個(gè)推論:S不等于0時(shí),當(dāng)寫(xiě)狀態(tài)(S&0x0000FFFF)等于0時(shí),則讀狀態(tài)(S>>>16)大于0,即讀鎖已被獲取。
通過(guò)ReentrantReadWriteLock的sync來(lái)實(shí)現(xiàn)
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 6317671515068378041L; 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; static int sharedCount(int c) { return c >>> SHARED_SHIFT; } static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
exclusiveCount(int c) 靜態(tài)方法,獲得持有寫(xiě)狀態(tài)的鎖的次數(shù)。
sharedCount(int c) 靜態(tài)方法,獲得持有讀狀態(tài)的鎖的線程數(shù)量。不同于寫(xiě)鎖,讀鎖可以同時(shí)被多個(gè)線程持有。而每個(gè)線程持有的讀鎖支持重入的特性,所以需要對(duì)每個(gè)線程持有的讀鎖的數(shù)量單獨(dú)計(jì)數(shù),這就需要用到 HoldCounter 計(jì)數(shù)器
HoldCounter 計(jì)數(shù)器
讀鎖的內(nèi)在機(jī)制其實(shí)就是一個(gè)共享鎖。一次共享鎖的操作就相當(dāng)于對(duì)HoldCounter 計(jì)數(shù)器的操作。獲取共享鎖,則該計(jì)數(shù)器 + 1,釋放共享鎖,該計(jì)數(shù)器 - 1。只有當(dāng)線程獲取共享鎖后才能對(duì)共享鎖進(jìn)行釋放、重入操作。
static final class HoldCounter { int count = 0; final long tid = getThreadId(Thread.currentThread()); } static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }
通過(guò) ThreadLocalHoldCounter 類(lèi),HoldCounter 與線程進(jìn)行綁定。HoldCounter 是綁定線程的一個(gè)計(jì)數(shù)器,而 ThreadLocalHoldCounter 則是線程綁定的 ThreadLocal。
- HoldCounter是用來(lái)記錄讀鎖重入數(shù)的對(duì)象
- ThreadLocalHoldCounter是ThreadLocal變量,用來(lái)存放不是第一個(gè)獲取讀鎖的線程的其他線程的讀鎖重入數(shù)對(duì)象
寫(xiě)鎖的獲取
寫(xiě)鎖是一個(gè)支持重進(jìn)入的排它鎖。如果當(dāng)前線程已經(jīng)獲取了寫(xiě)鎖,則增加寫(xiě)狀態(tài)。如果當(dāng)前線程在獲取寫(xiě)鎖時(shí),讀鎖已經(jīng)被獲?。ㄗx狀態(tài)不為0)或者該線程不是已經(jīng)獲取寫(xiě)鎖的線程, 則當(dāng)前線程進(jìn)入等待狀態(tài)。
寫(xiě)鎖的獲取是通過(guò)重寫(xiě)AQS中的tryAcquire方法實(shí)現(xiàn)的。
protected final boolean tryAcquire(int acquires) { //當(dāng)前線程 Thread current = Thread.currentThread(); //獲取state狀態(tài) 存在讀鎖或者寫(xiě)鎖,狀態(tài)就不為0 int c = getState(); //獲取寫(xiě)鎖的重入數(shù) int w = exclusiveCount(c); //當(dāng)前同步狀態(tài)state != 0,說(shuō)明已經(jīng)有其他線程獲取了讀鎖或?qū)戞i if (c != 0) { // c!=0 && w==0 表示存在讀鎖 // 當(dāng)前存在讀鎖或者寫(xiě)鎖已經(jīng)被其他寫(xiě)線程獲取,則寫(xiě)鎖獲取失敗 if (w == 0 || current != getExclusiveOwnerThread()) return false; // 超出最大范圍 65535 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); //同步state狀態(tài) setState(c + acquires); return true; } // writerShouldBlock有公平與非公平的實(shí)現(xiàn), 非公平返回false,會(huì)嘗試通過(guò)cas加鎖 //c==0 寫(xiě)鎖未被任何線程獲取,當(dāng)前線程是否阻塞或者cas嘗試獲取鎖 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //設(shè)置寫(xiě)鎖為當(dāng)前線程所有 setExclusiveOwnerThread(current); return true;
通過(guò)源碼我們可以知道:
- 讀寫(xiě)互斥
- 寫(xiě)寫(xiě)互斥
- 寫(xiě)鎖支持同一個(gè)線程重入
- writerShouldBlock寫(xiě)鎖是否阻塞實(shí)現(xiàn)取決公平與非公平的策略(FairSync和NonfairSync)
大致流程如下:
寫(xiě)鎖的釋放
寫(xiě)鎖釋放通過(guò)重寫(xiě)AQS的tryRelease方法實(shí)現(xiàn)
protected final boolean tryRelease(int releases) { //若鎖的持有者不是當(dāng)前線程,拋出異常 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; //當(dāng)前寫(xiě)狀態(tài)是否為0,為0則釋放寫(xiě)鎖 boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free;
流程如下:
讀鎖的獲取
實(shí)現(xiàn)共享式同步組件的同步語(yǔ)義需要通過(guò)重寫(xiě)AQS的tryAcquireShared方法和tryReleaseShared方法。讀鎖的獲取實(shí)現(xiàn)方法為:
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); // 如果寫(xiě)鎖已經(jīng)被獲取并且獲取寫(xiě)鎖的線程不是當(dāng)前線程,當(dāng)前線程獲取讀鎖失敗返回-1 判斷鎖降級(jí) if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //計(jì)算出讀鎖的數(shù)量 int r = sharedCount(c); /** * 讀鎖是否阻塞 readerShouldBlock()公平與非公平的實(shí)現(xiàn) * r < MAX_COUNT: 持有讀鎖的線程小于最大數(shù)(65535) * compareAndSetState(c, c + SHARED_UNIT) cas設(shè)置獲取讀鎖線程的數(shù)量 */ if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { //當(dāng)前線程獲取讀鎖 if (r == 0) { //設(shè)置第一個(gè)獲取讀鎖的線程 firstReader = current; firstReaderHoldCount = 1; //設(shè)置第一個(gè)獲取讀鎖線程的重入數(shù) } else if (firstReader == current) { // 表示第一個(gè)獲取讀鎖的線程重入 firstReaderHoldCount++; } else { // 非第一個(gè)獲取讀鎖的線程 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; //記錄其他獲取讀鎖的線程的重入次數(shù) } return 1; } // 嘗試通過(guò)自旋的方式獲取讀鎖,實(shí)現(xiàn)了重入邏輯 return fullTryAcquireShared(current);
- 讀鎖共享,讀讀不互斥
- 讀鎖可重入,每個(gè)獲取讀鎖的線程都會(huì)記錄對(duì)應(yīng)的重入數(shù)
- 讀寫(xiě)互斥,鎖降級(jí)場(chǎng)景除外
- 支持鎖降級(jí),持有寫(xiě)鎖的線程,可以獲取讀鎖,但是后續(xù)要記得把讀鎖和寫(xiě)鎖讀釋放
- readerShouldBlock讀鎖是否阻塞實(shí)現(xiàn)取決公平與非公平的策略(FairSync和NonfairSync)
流程如下:
讀鎖的釋放
獲取到讀鎖,執(zhí)行完臨界區(qū)后,要記得釋放讀鎖(如果重入多次要釋放對(duì)應(yīng)的次數(shù)),不然會(huì)阻塞其他線程的寫(xiě)操作。
讀鎖釋放的實(shí)現(xiàn)主要通過(guò)方法tryReleaseShared:
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); //如果當(dāng)前線程是第一個(gè)獲取讀鎖的線程 if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; //重入次數(shù)減1 } else { //不是第一個(gè)獲取讀鎖的線程 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; //重入次數(shù)減1 } for (;;) { //cas更新同步狀態(tài) 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; }
流程如下:
七.總結(jié)
本文主要講解ReentrantReadWriteLock的使用,讀寫(xiě)鎖設(shè)計(jì)的原理,鎖降級(jí),應(yīng)用場(chǎng)景及源碼解析,重點(diǎn)解析了寫(xiě)鎖的獲取和釋放,讀鎖的獲取和釋放,深層次的理解讀寫(xiě)鎖是怎樣實(shí)現(xiàn)分別記錄讀寫(xiě)狀態(tài)的,以及讀寫(xiě)鎖的獲取及釋放。
到此這篇關(guān)于Java AQS中ReentrantReadWriteLock讀寫(xiě)鎖的使用的文章就介紹到這了,更多相關(guān)Java ReentrantReadWriteLock內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java并發(fā)讀寫(xiě)鎖ReentrantReadWriteLock 使用場(chǎng)景
- Java中的讀寫(xiě)鎖ReentrantReadWriteLock源碼分析
- 一文了解Java讀寫(xiě)鎖ReentrantReadWriteLock的使用
- 詳解Java?ReentrantReadWriteLock讀寫(xiě)鎖的原理與實(shí)現(xiàn)
- ReentrantReadWriteLock?讀寫(xiě)鎖分析總結(jié)
- Java多線程讀寫(xiě)鎖ReentrantReadWriteLock類(lèi)詳解
- Java中ReentrantReadWriteLock讀寫(xiě)鎖的實(shí)現(xiàn)
相關(guān)文章
詳解Intellij IDEA 2017 debug斷點(diǎn)調(diào)試技巧(總結(jié))
這篇文章主要介紹了詳解Intellij IDEA 2017 debug斷點(diǎn)調(diào)試技巧(總結(jié)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11SpringBoot中的@ConditionalOnMissingBean注解使用詳解
這篇文章主要介紹了SpringBoot中的@ConditionalOnMissingBean注解使用詳解,@ConditionalOnMissingBean作用在@Bean定義上,也就是說(shuō)在容器加載它作用的Bean時(shí),檢查容器中是否存在目標(biāo)類(lèi)型,需要的朋友可以參考下2024-01-01SpringBoot預(yù)防XSS攻擊的實(shí)現(xiàn)
XSS攻擊是一種在web應(yīng)用中的計(jì)算機(jī)安全漏洞,它允許惡意web用戶將代碼植入到提供給其它用戶使用的頁(yè)面,本文主要介紹了SpringBoot預(yù)防XSS攻擊的實(shí)現(xiàn),感興趣的可以了解一下2023-08-08Spring:@Async注解和AsyncResult與CompletableFuture使用問(wèn)題
這篇文章主要介紹了Spring:@Async注解和AsyncResult與CompletableFuture使用問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08最優(yōu)雅地整合 Spring & Spring MVC & MyBatis 搭建 Java 企業(yè)級(jí)應(yīng)用(附源碼)
這篇文章主要介紹了最優(yōu)雅地整合 Spring & Spring MVC & MyBatis 搭建 Java 企業(yè)級(jí)應(yīng)用(附源碼),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01詳解在springmvc中解決FastJson循環(huán)引用的問(wèn)題
本篇文章主要介紹了在springmvc中解決FastJson循環(huán)引用的問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01