Java讀寫鎖ReadWriteLock原理與應用場景詳解
Java并發(fā)編程提供了讀寫鎖,主要用于讀多寫少的場景
什么是讀寫鎖?
讀寫鎖并不是JAVA所特有的讀寫鎖(Readers-Writer Lock)顧名思義是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個線程同時獲得,因為讀操作本身是線程安全的,而寫鎖則是互斥鎖,不允許多個線程同時獲得寫鎖,并且寫操作和讀操作也是互斥的。
所謂的讀寫鎖(Readers-Writer Lock),顧名思義就是將一個鎖拆分為讀鎖和寫鎖兩個鎖。
其中讀鎖允許多個線程同時獲得,而寫鎖則是互斥鎖,不允許多個線程同時獲得寫鎖,并且寫操作和讀操作也是互斥的。
為什么需要讀寫鎖?
Synchronized 和 ReentrantLock 都是獨占鎖,即在同一時刻只有一個線程獲取到鎖。
然而在有些業(yè)務場景中,我們大多在讀取數(shù)據(jù),很少寫入數(shù)據(jù),這種情況下,如果仍使用獨占鎖,效率將及其低下。
針對這種情況,Java提供了讀寫鎖——ReentrantReadWriteLock。
主要解決:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁的場景。
讀寫鎖的特點
- 公平性:讀寫鎖支持非公平和公平的鎖獲取方式,非公平鎖的吞吐量優(yōu)于公平鎖的吞吐量,默認構造的是非公平鎖
- 可重入:在線程獲取讀鎖之后能夠再次獲取讀鎖,但是不能獲取寫鎖,而線程在獲取寫鎖之后能夠再次獲取寫鎖,同時也能獲取讀鎖
- 鎖降級:線程獲取寫鎖之后獲取讀鎖,再釋放寫鎖,這樣實現(xiàn)了寫鎖變?yōu)樽x鎖,也叫鎖降級
讀寫鎖的使用場景
ReentrantReadWriteLock適合讀多寫少的場景:
讀鎖ReentrantReadWriteLock.ReadLock可以被多個線程同時持有, 所以并發(fā)能力很高。
寫鎖ReentrantReadWriteLock.WriteLock是獨占鎖, 在一個線程持有寫鎖時候, 其他線程都不能在搶占, 包含搶占讀鎖都會阻塞。
ReentrantReadWriteLock的使用場景總結(jié):其實就是 讀讀并發(fā)、讀寫互斥、寫寫互斥而已,如果一個對象并發(fā)讀的場景大于并發(fā)寫的場景,那就可以使用 ReentrantReadWriteLock來達到保證線程安全的前提下提高并發(fā)效率。
讀寫鎖的主要成員和結(jié)構圖
1. ReentrantReadWriteLock的繼承關系
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(); }
讀寫鎖 ReadWriteLock
讀寫鎖維護了一對相關的鎖,一個用于只讀操作,一個用于寫入操作。
只要沒有寫入,讀取鎖可以由多個讀線程同時保持,寫入鎖是獨占的。
2.ReentrantReadWriteLock的核心變量
ReentrantReadWriteLock類包含三個核心變量:
- ReaderLock:讀鎖,實現(xiàn)了Lock接口
- WriterLock:寫鎖,也實現(xiàn)了Lock接口
- Sync:繼承自AbstractQueuedSynchronize(AQS),可以為公平鎖FairSync 或 非公平鎖NonfairSync
3.ReentrantReadWriteLock的成員變量和構造函數(shù)
/** 內(nèi)部提供的讀鎖 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 內(nèi)部提供的寫鎖 */ private final ReentrantReadWriteLock.WriteLock writerLock; /** AQS來實現(xiàn)的同步器 */ final Sync sync; /** * Creates a new {@code ReentrantReadWriteLock} with * 默認創(chuàng)建非公平的讀寫鎖 */ public ReentrantReadWriteLock() { this(false); } /** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
讀寫鎖的實現(xiàn)原理
ReentrantReadWriteLock實現(xiàn)關鍵點,主要包括:
- 讀寫狀態(tài)的設計
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級
1.讀寫狀態(tài)的設計
之前談ReentrantLock的時候,Sync類是繼承于AQS,主要以int state為線程鎖狀態(tài),0表示沒有被線程占用,1表示已經(jīng)有線程占用。
同樣ReentrantReadWriteLock也是繼承于AQS來實現(xiàn)同步,那int state怎樣同時來區(qū)分讀鎖和寫鎖的?
如果在一個整型變量上維護多種狀態(tài),就一定需要“按位切割使用”這個變量,ReentrantReadWriteLock將int類型的state將變量切割成兩部分:
- 高16位記錄讀鎖狀態(tài)
- 低16位記錄寫鎖狀態(tài)
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); // 讀鎖最大數(shù)量 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 寫鎖最大數(shù)量 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 本地線程計數(shù)器 private transient ThreadLocalHoldCounter readHolds; // 緩存的計數(shù)器 private transient HoldCounter cachedHoldCounter; // 第一個讀線程 private transient Thread firstReader = null; // 第一個讀線程的計數(shù) private transient int firstReaderHoldCount; }
2.寫鎖的獲取與釋放
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(); //獲取獨占鎖(寫鎖)的被獲取的數(shù)量 int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) //1.如果同步狀態(tài)不為0,且寫狀態(tài)為0,則表示當前同步狀態(tài)被讀鎖獲取 //2.或者當前擁有寫鎖的線程不是當前線程 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; setExclusiveOwnerThread(current); return true; }
1)c是獲取當前鎖狀態(tài),w是獲取寫鎖的狀態(tài)。
2)如果鎖狀態(tài)不為零,而寫鎖的狀態(tài)為0,則表示讀鎖狀態(tài)不為0,所以當前線程不能獲取寫鎖。或者鎖狀態(tài)不為零,而寫鎖的狀態(tài)也不為0,但是獲取寫鎖的線程不是當前線程,則當前線程不能獲取寫鎖。
3)寫鎖是一個可重入的排它鎖,在獲取同步狀態(tài)時,增加了一個讀鎖是否存在的判斷。
寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態(tài)減1,直到寫狀態(tài)為0時,才表示該寫鎖被釋放了。
3.讀鎖的獲取與釋放
protected final int tryAcquireShared(int unused) { for(;;) { int c = getState(); int nextc = c + (1<<16); if(nextc < c) { throw new Error("Maxumum lock count exceeded"); } if(exclusiveCount(c)!=0 && owner != Thread.currentThread()) return -1; if(compareAndSetState(c,nextc)) return 1; } }
1)讀鎖是一個支持重進入的共享鎖,可以被多個線程同時獲取。
2)在沒有寫狀態(tài)為0時,讀鎖總會被成功獲取,而所做的也只是增加讀狀態(tài)(線程安全)
3)讀狀態(tài)是所有線程獲取讀鎖次數(shù)的總和,而每個線程各自獲取讀鎖的次數(shù)只能選擇保存在ThreadLocal中,由線程自身維護。
讀鎖的每次釋放均減小狀態(tài)(線程安全的,可能有多個讀線程同時釋放鎖),減小的值是1<<16。
4.鎖降級
降級是指當前把持住寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。
鎖降級過程中的讀鎖的獲取是否有必要,答案是必要的。主要是為了保證數(shù)據(jù)的可見性,如果當前線程不獲取讀鎖而直接釋放寫鎖,假設此刻另一個線程獲取的寫鎖,并修改了數(shù)據(jù),那么當前線程就步伐感知到線程T的數(shù)據(jù)更新,如果當前線程遵循鎖降級的步驟,那么線程T將會被阻塞,直到當前線程使數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進行數(shù)據(jù)更新。
5.讀鎖與寫鎖的整體流程
讀寫鎖總結(jié)
本篇詳細介紹了ReentrantReadWriteLock的特征、實現(xiàn)、鎖的獲取過程,通過4個關鍵點的核心設計:
- 讀寫狀態(tài)的設計
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級
從而才能實現(xiàn):共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁的應用場景。
以上就是Java讀寫鎖ReadWriteLock原理與應用場景詳解的詳細內(nèi)容,更多關于Java讀寫鎖ReadWriteLock原理與應用場景的資料請關注腳本之家其它相關文章!
相關文章
輸出java進程的jstack信息示例分享 通過線程堆棧信息分析java線程
通過ps到java進程號將進程的jstack信息輸出。jstack信息是java進程的線程堆棧信息,通過該信息可以分析java的線程阻塞等問題。2014-01-01解決maven?maven.compiler.source和maven.compiler.target的坑
這篇文章主要介紹了解決maven?maven.compiler.source和maven.compiler.target的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12spring AOP的Around增強實現(xiàn)方法分析
這篇文章主要介紹了spring AOP的Around增強實現(xiàn)方法,結(jié)合實例形式分析了spring面向切面AOP的Around增強具體步驟與相關操作方法,需要的朋友可以參考下2020-01-01