Java中讀寫鎖ReadWriteLock的原理與應(yīng)用詳解
Java并發(fā)編程提供了讀寫鎖,主要用于讀多寫少的場景,今天我就重點(diǎn)來講解讀寫鎖的底層實(shí)現(xiàn)原理
什么是讀寫鎖?
讀寫鎖并不是JAVA所特有的讀寫鎖(Readers-Writer Lock)顧名思義是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個(gè)線程同時(shí)獲得,因?yàn)樽x操作本身是線程安全的,而寫鎖則是互斥鎖,不允許多個(gè)線程同時(shí)獲得寫鎖,并且寫操作和讀操作也是互斥的。
所謂的讀寫鎖(Readers-Writer Lock),顧名思義就是將一個(gè)鎖拆分為讀鎖和寫鎖兩個(gè)鎖。
其中讀鎖允許多個(gè)線程同時(shí)獲得,而寫鎖則是互斥鎖,不允許多個(gè)線程同時(shí)獲得寫鎖,并且寫操作和讀操作也是互斥的。

為什么需要讀寫鎖?
Synchronized 和 ReentrantLock 都是獨(dú)占鎖,即在同一時(shí)刻只有一個(gè)線程獲取到鎖。
然而在有些業(yè)務(wù)場景中,我們大多在讀取數(shù)據(jù),很少寫入數(shù)據(jù),這種情況下,如果仍使用獨(dú)占鎖,效率將及其低下。
針對(duì)這種情況,Java提供了讀寫鎖——ReentrantReadWriteLock。
主要解決:對(duì)共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁的場景。
讀寫鎖的特點(diǎn)
- 公平性:讀寫鎖支持非公平和公平的鎖獲取方式,非公平鎖的吞吐量優(yōu)于公平鎖的吞吐量,默認(rèn)構(gòu)造的是非公平鎖
- 可重入:在線程獲取讀鎖之后能夠再次獲取讀鎖,但是不能獲取寫鎖,而線程在獲取寫鎖之后能夠再次獲取寫鎖,同時(shí)也能獲取讀鎖
- 鎖降級(jí):線程獲取寫鎖之后獲取讀鎖,再釋放寫鎖,這樣實(shí)現(xiàn)了寫鎖變?yōu)樽x鎖,也叫鎖降級(jí)
讀寫鎖的使用場景
ReentrantReadWriteLock適合讀多寫少的場景:
讀鎖ReentrantReadWriteLock.ReadLock可以被多個(gè)線程同時(shí)持有, 所以并發(fā)能力很高。
寫鎖ReentrantReadWriteLock.WriteLock是獨(dú)占鎖, 在一個(gè)線程持有寫鎖時(shí)候, 其他線程都不能在搶占, 包含搶占讀鎖都會(huì)阻塞。
ReentrantReadWriteLock的使用場景總結(jié):其實(shí)就是 讀讀并發(fā)、讀寫互斥、寫寫互斥而已,如果一個(gè)對(duì)象并發(fā)讀的場景大于并發(fā)寫的場景,那就可以使用 ReentrantReadWriteLock來達(dá)到保證線程安全的前提下提高并發(fā)效率。
讀寫鎖的主要成員和結(jié)構(gòu)圖
1. ReentrantReadWriteLock的繼承關(guān)系

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
讀寫鎖維護(hù)了一對(duì)相關(guān)的鎖,一個(gè)用于只讀操作,一個(gè)用于寫入操作。
只要沒有寫入,讀取鎖可以由多個(gè)讀線程同時(shí)保持,寫入鎖是獨(dú)占的。
2.ReentrantReadWriteLock的核心變量

ReentrantReadWriteLock類包含三個(gè)核心變量:
- ReaderLock:讀鎖,實(shí)現(xiàn)了Lock接口
- WriterLock:寫鎖,也實(shí)現(xiàn)了Lock接口
- Sync:繼承自AbstractQueuedSynchronize(AQS),可以為公平鎖FairSync 或 非公平鎖NonfairSync
3.ReentrantReadWriteLock的成員變量和構(gòu)造函數(shù)
/** 內(nèi)部提供的讀鎖 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 內(nèi)部提供的寫鎖 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** AQS來實(shí)現(xiàn)的同步器 */
final Sync sync;
/**
* Creates a new {@code ReentrantReadWriteLock} with
* 默認(rèn)創(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);
}讀寫鎖的實(shí)現(xiàn)原理
ReentrantReadWriteLock實(shí)現(xiàn)關(guān)鍵點(diǎn),主要包括:
- 讀寫狀態(tài)的設(shè)計(jì)
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級(jí)
1.讀寫狀態(tài)的設(shè)計(jì)
之前談ReentrantLock的時(shí)候,Sync類是繼承于AQS,主要以int state為線程鎖狀態(tài),0表示沒有被線程占用,1表示已經(jīng)有線程占用。
同樣ReentrantReadWriteLock也是繼承于AQS來實(shí)現(xiàn)同步,那int state怎樣同時(shí)來區(qū)分讀鎖和寫鎖的?
如果在一個(gè)整型變量上維護(hù)多種狀態(tài),就一定需要“按位切割使用”這個(gè)變量,ReentrantReadWriteLock將int類型的state將變量切割成兩部分:
- 高16位記錄讀鎖狀態(tài)
- 低16位記錄寫鎖狀態(tài)

abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列號(hào)
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;
// 本地線程計(jì)數(shù)器
private transient ThreadLocalHoldCounter readHolds;
// 緩存的計(jì)數(shù)器
private transient HoldCounter cachedHoldCounter;
// 第一個(gè)讀線程
private transient Thread firstReader = null;
// 第一個(gè)讀線程的計(jì)數(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();
//獲取獨(dú)占鎖(寫鎖)的被獲取的數(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,則表示當(dāng)前同步狀態(tài)被讀鎖獲取
//2.或者當(dāng)前擁有寫鎖的線程不是當(dāng)前線程
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是獲取當(dāng)前鎖狀態(tài),w是獲取寫鎖的狀態(tài)。
2)如果鎖狀態(tài)不為零,而寫鎖的狀態(tài)為0,則表示讀鎖狀態(tài)不為0,所以當(dāng)前線程不能獲取寫鎖?;蛘哝i狀態(tài)不為零,而寫鎖的狀態(tài)也不為0,但是獲取寫鎖的線程不是當(dāng)前線程,則當(dāng)前線程不能獲取寫鎖。
3)寫鎖是一個(gè)可重入的排它鎖,在獲取同步狀態(tài)時(shí),增加了一個(gè)讀鎖是否存在的判斷。
寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態(tài)減1,直到寫狀態(tài)為0時(shí),才表示該寫鎖被釋放了。
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)讀鎖是一個(gè)支持重進(jìn)入的共享鎖,可以被多個(gè)線程同時(shí)獲取。
2)在沒有寫狀態(tài)為0時(shí),讀鎖總會(huì)被成功獲取,而所做的也只是增加讀狀態(tài)(線程安全)
3)讀狀態(tài)是所有線程獲取讀鎖次數(shù)的總和,而每個(gè)線程各自獲取讀鎖的次數(shù)只能選擇保存在ThreadLocal中,由線程自身維護(hù)。
讀鎖的每次釋放均減小狀態(tài)(線程安全的,可能有多個(gè)讀線程同時(shí)釋放鎖),減小的值是1<<16。
4.鎖降級(jí)
降級(jí)是指當(dāng)前把持住寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。
鎖降級(jí)過程中的讀鎖的獲取是否有必要,答案是必要的。主要是為了保證數(shù)據(jù)的可見性,如果當(dāng)前線程不獲取讀鎖而直接釋放寫鎖,假設(shè)此刻另一個(gè)線程獲取的寫鎖,并修改了數(shù)據(jù),那么當(dāng)前線程就步伐感知到線程T的數(shù)據(jù)更新,如果當(dāng)前線程遵循鎖降級(jí)的步驟,那么線程T將會(huì)被阻塞,直到當(dāng)前線程使數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進(jìn)行數(shù)據(jù)更新。
5.讀鎖與寫鎖的整體流程

讀寫鎖總結(jié)
本篇詳細(xì)介紹了ReentrantReadWriteLock的特征、實(shí)現(xiàn)、鎖的獲取過程,通過4個(gè)關(guān)鍵點(diǎn)的核心設(shè)計(jì):
- 讀寫狀態(tài)的設(shè)計(jì)
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級(jí)
從而才能實(shí)現(xiàn):共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁的應(yīng)用場景。
以上就是Java中讀寫鎖ReadWriteLock的原理與應(yīng)用詳解的詳細(xì)內(nèi)容,更多關(guān)于Java讀寫鎖ReadWriteLock的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot開發(fā)存儲(chǔ)服務(wù)器實(shí)現(xiàn)過程詳解
這篇文章主要為大家介紹了SpringBoot開發(fā)存儲(chǔ)服務(wù)器實(shí)現(xiàn)過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
JAVA實(shí)現(xiàn)經(jīng)典掃雷游戲的示例代碼
windows自帶的游戲《掃雷》是陪伴了無數(shù)人的經(jīng)典游戲,本程序參考《掃雷》的規(guī)則進(jìn)行了簡化,用java語言實(shí)現(xiàn),采用了swing技術(shù)進(jìn)行了界面化處理。感興趣的可以學(xué)習(xí)一下2022-01-01
詳解SpringBoot之訪問靜態(tài)資源(webapp...)
這篇文章主要介紹了詳解SpringBoot之訪問靜態(tài)資源(webapp...),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
將一個(gè)數(shù)組按照固定大小進(jìn)行拆分成數(shù)組的方法
下面小編就為大家?guī)硪黄獙⒁粋€(gè)數(shù)組按照固定大小進(jìn)行拆分成數(shù)組的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-11-11
Java連接FTP服務(wù)器并使用ftp連接池進(jìn)行文件操作指南
使用FTP最主要的功能是對(duì)文件進(jìn)行管理,下面這篇文章主要給大家介紹了關(guān)于Java連接FTP服務(wù)器并使用ftp連接池進(jìn)行文件操作的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08
Java發(fā)送https請(qǐng)求代碼實(shí)例
這篇文章主要介紹了Java發(fā)送https請(qǐng)求代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08

