Java中ReentrantReadWriteLock讀寫鎖的實現(xiàn)
一、鎖的分類
這里不會對Java中大部分的分類都聊清楚,主要把 **互斥,共享** 這種分類聊清楚。
Java中的互斥鎖,synchronized,ReentrantLock這種都是互斥鎖。一個線程持有鎖操作時,其他線程都需要等待前面的線程釋放鎖資源,才能重新嘗試競爭這把鎖。
Java中的讀寫鎖(支撐互斥&共享),Java中最常見的就是 **ReentrantReadWriteLock** ,StampedLock。
其中StampedLock是JDK1.8中推出的一款讀寫鎖的實現(xiàn),針對ReentrantReadWriteLock一個優(yōu)化。但是,今兒不細聊。主要玩ReentrantReadWriteLock。
ReentrantReadWriteLock主要就是解決咱們剛才聊的,讀寫操作都有,讀操作居多,寫操作頻次相對比較低的情況,可以使用讀寫鎖來提升系統(tǒng)性能。
讀寫鎖中:
* 寫寫互斥
* 讀寫互斥
* 寫讀互斥
* 讀讀共享
* 有鎖降級的情況,后面聊??!
二、ReentrantReadWriteLock的基本操作
ReentrantReadWriteLock中實現(xiàn)了ReadWriteLock的接口,在這個接口里面提供了兩個抽象方法。
正常的操作,是new ReentrantReadWriteLock的對象,但是你具體的業(yè)務(wù)操作是需要讀鎖,還是寫鎖,你需要單獨的獲取到,然后針對性的加鎖。
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();
}具體使用方式
public static void main(String[] args){
// 1、構(gòu)建讀寫鎖對象
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 2、單獨獲取讀、寫鎖對象
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 3、根據(jù)業(yè)務(wù)使用具體的鎖對象加鎖
writeLock.lock();
// try-finally的目的,是為了避免沒有及時釋放鎖資源導(dǎo)致死鎖的問題。
try{
// 4、業(yè)務(wù)操作…………
System.out.println("寫操作");
}finally {
// 5、釋放鎖
writeLock.unlock();
}
}三、ReentrantReadWriteLock的底層實現(xiàn)
ReentrantReadWriteLock是基于AQS實現(xiàn)的。
AQS是JUC包下的一個抽象類AbstractQueuedSynchronizer
暫時只關(guān)注兩點,分別是AQS提供的state屬性,還有AQS提供的一個同步隊列。
state屬性,用來標識當前 讀寫鎖 的資源是否被占用的核心標識。
private volatile int state;
一個int類型的state,是4字節(jié),每個字節(jié)占用8個bit位,一個state占用32個bit位。
* 高16位,作為讀鎖的標記。
* 低16位,作為寫鎖的標記。
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
00000000 00000000 11111111 11111111
/** 查看讀鎖的占用情況。 */
static int sharedCount(int state) { return state >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int state) { return state & EXCLUSIVE_MASK; }
00000000 00000000 00000000 00000000 int類型的數(shù)值的32個bit位。
讀鎖占用情況:
00000000 00000011 00000000 00000000 state
>>> 16
00000000 00000000 00000000 00000011 讀鎖被獲取了三次。
寫鎖占用情況。(這里之所以&這個么東西,是對后期的鎖降級有影響~)
00000000 00000000 00000000 00000001 state
&
00000000 00000000 11111111 11111111
=
00000000 00000000 00000000 00000001 寫鎖被獲取了一次。一個同步隊列,當線程獲取鎖資源失敗時,需要到這個同步隊列中排隊。到了合適的時機,就會繼續(xù)嘗試獲取對應(yīng)的鎖資源。

四、ReentrantReadWriteLock的鎖重入
同一個線程,多次獲取同一把鎖時,就會出現(xiàn)鎖重入的情況。
而咱們大多數(shù)的鎖,都會提供鎖重入的功能。
鎖重入場景:
public class Demo {
// 1、構(gòu)建讀寫鎖對象
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 2、單獨獲取讀、寫鎖對象
static ReentrantReadWriteLock.ReadLock readLock;
static ReentrantReadWriteLock.WriteLock writeLock;
static{
// 2、單獨獲取讀、寫鎖對象
readLock = readWriteLock.readLock();
writeLock = readWriteLock.writeLock();
}
public static void main(String[] args){
// 3、根據(jù)業(yè)務(wù)使用具體的鎖對象加鎖
writeLock.lock();
// try-finally的目的,是為了避免沒有及時釋放鎖資源導(dǎo)致死鎖的問題。
try{
// 4、業(yè)務(wù)操作…………調(diào)用其他方法
xxx();
}finally {
// 5、釋放鎖
writeLock.unlock();
}
}
private static void xxx(){
writeLock.lock();
try{
// 其他按業(yè)務(wù)
}finally {
writeLock.unlock();
}
}
}咱們底層的鎖重入邏輯很簡單
**寫鎖:** 寫鎖的實現(xiàn)就是每一次獲取寫鎖時,會對state的低16位+1,再次獲取,再次+1。同理,每次釋放鎖資源時,也需要對state進行-1。 而當對state的低16位減到0時,鎖資源就釋放干凈了。

讀鎖: 首先,讀鎖是共享的,他用state的高16位來維護信息。如果高16位的state的值,經(jīng)過運算,知道了是4,也就是讀鎖被獲取了4次??赡蹵線程獲取了2次讀鎖資源。 B線程獲取了2次讀鎖資源。高位的state自然就是4。但是因為程序員寫代碼除了問題,使用A線程,釋放了4次讀鎖資源,那此時B線程是不是就可能出現(xiàn)數(shù)據(jù)安全問題了。
所以,為了解決上述的問題,每個線程需要獨立的記錄自己獲取了幾次讀鎖資源??梢允褂肨hreadLocal來保存線程局部的信息,每次加鎖時,ThreadLocal中需要存儲一個標記,每次+1。每次釋放鎖時,也需要將ThreadLocal中的標記進行-1。讀線程最后是基于自己的ThreadLocal中的數(shù)值,來確認讀鎖是否釋放干凈。


五、ReentrantReadWriteLock的寫鎖饑餓
寫鎖饑餓的問題。
如果寫線程在AQS中排隊,并且排在head.next的位置。 那么其他想獲取讀鎖的讀線程需要排隊。避免大量的讀請求獲取讀鎖,讓寫線程一直AQS隊列中排隊,無法執(zhí)行寫操作的問題。
通過源碼可以看到,讀寫鎖中,僅僅針對head.next這個節(jié)點的情況,來確認讀線程獲取讀鎖時是否需要排隊
// 這個方法,總結(jié)一句話。
// AQS中有排隊的Node,并且head的next節(jié)點是一個有線程并且在等待寫鎖的Node
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
ReentrantReadWriteLock讀寫鎖中有鎖降級,但是這個和synchronized的鎖升級沒任何關(guān)系!?。?/p>
六、ReentrantReadWriteLock的鎖降級
ReentrantReadWriteLock的鎖降級是指當前線程如果持有了寫鎖,可以降級直接獲取到讀鎖。
在讀寫鎖中,持有寫鎖的同時,再去獲取讀鎖,這種行為一般被稱為 **鎖降級** 。
在讀寫鎖中,持有讀鎖的同時,去獲取寫鎖,這種行為被稱為 **鎖升級** ,這個行為是不允許的。
這里是獲取讀鎖的的邏輯,看一下鎖降級的支持方式
// 競爭讀鎖。
if (exclusiveCount(c) != 0 && // 這行代表某個線程持有寫鎖
getExclusiveOwnerThread() != current) // 這行代表持有寫鎖的不是當前線程
// 退出競爭,無法獲取讀鎖
return -1; 前面邏輯沒有走return - 1之后,在后續(xù)就會正常的對state的高位+1,并且完成讀鎖的計數(shù)操作。
七、ReentrantReadWriteLock的優(yōu)化
ReentrantReadWriteLock的優(yōu)化主要是在讀鎖計數(shù)層面上做的優(yōu)化。
這個對性能的優(yōu)化微乎其微,但是確確實實是一個優(yōu)化。
在獲取讀鎖時,因為是共享的,這種優(yōu)化只針對第一個獲取讀鎖的線程和最后一個獲取讀鎖的線程。
針對第一個獲取讀鎖的線程,他采用一個全局變量記錄重入次數(shù)。這個操作可以節(jié)省掉使用ThreadLocal的時間成本和內(nèi)存成本。

其中firstReader記錄第一個獲取讀鎖的線程。
firstReaderHoldCount,記錄第一個獲取讀鎖的線程的重入次數(shù)。
這里是最后一個獲取讀鎖的線程需要走的邏輯
cachedHoldCounter這個屬性是記錄最后一個獲取讀鎖的線程的重入次數(shù)。
這里可以讓最后一個獲取讀鎖的線程在重入時,省略掉去ThreadLocal中g(shù)et計數(shù)器的操作,但是之前的set存儲操作,不能省略
// 獲取上次最后獲取讀鎖的線程
HoldCounter rh = cachedHoldCounter;
// 查看當前線程是否是之前的cachedHoldCounter
if (rh == null || rh.tid != getThreadId(current))
// 說明不是,將當前獲取讀鎖的線程設(shè)置為cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
// 這個判斷代表第一次獲取讀鎖才會進去
else if (rh.count == 0)
// 如果是第一次獲取讀鎖,不是重入,還是需要扔到ThreadLocal里紀錄好,。
readHolds.set(rh);
// 直接對獲取到的rh做++操作,代表獲取了一次讀鎖。
rh.count++;到此這篇關(guān)于Java中ReentrantReadWriteLock讀寫鎖的實現(xiàn)的文章就介紹到這了,更多相關(guān)ReentrantReadWriteLock讀寫鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
將Java(SpringBoot)項目打包為Docker鏡像的三種方法
這篇文章主要介紹了將Java(SpringBoot)項目打包為Docker鏡像的三種方法,分別是手動構(gòu)建、使用Dockerfile和使用SpringBootMaven插件,每種方法都有其特點和適用場景,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-03-03
Java連接mysql數(shù)據(jù)庫以及mysql驅(qū)動jar包下載和使用方法
這篇文章主要給大家介紹了關(guān)于Java連接mysql數(shù)據(jù)庫以及mysql驅(qū)動jar包下載和使用方法,MySQL是一款常用的關(guān)系型數(shù)據(jù)庫,它的JDBC驅(qū)動程序使得我們可以通過Java程序連接MySQL數(shù)據(jù)庫進行數(shù)據(jù)操作,需要的朋友可以參考下2023-11-11
Java8 實現(xiàn)stream將對象集合list中抽取屬性集合轉(zhuǎn)化為map或list
這篇文章主要介紹了Java8 實現(xiàn)stream將對象集合list中抽取屬性集合轉(zhuǎn)化為map或list的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Maven包沖突導(dǎo)致NoSuchMethodError錯誤的解決辦法
web 項目 能正常編譯,運行時也正常啟動,但執(zhí)行到需要調(diào)用 org.codehaus.jackson 包中的某個方法時,產(chǎn)生運行異常,這篇文章主要介紹了Maven包沖突導(dǎo)致NoSuchMethodError錯誤的解決辦法,需要的朋友可以參考下2024-05-05
SpringBoot實現(xiàn)阿里云快遞物流查詢的示例代碼
本文將基于springboot實現(xiàn)快遞物流查詢,物流信息的獲取通過阿里云第三方實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2021-10-10
nodejs與JAVA應(yīng)對高并發(fā)的對比方式
這篇文章主要介紹了nodejs與JAVA應(yīng)對高并發(fā)的對比方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08

