「原創」Java並發編程系列18|讀寫鎖(下)

2020-12-12 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。

通過以下幾部分來分析Java提供的讀寫鎖ReentrantReadWriteLock:

為什麼需要讀寫鎖讀寫鎖的使用DemoReentrantReadWriteLock類結構記錄讀寫鎖狀態源碼分析讀鎖的獲取與釋放源碼分析寫鎖的獲取與釋放鎖降級讀寫鎖應用本文涉及到上下文聯繫較多,經常需要上下滑動查看,篇幅太多很不方便,而且文章太長閱讀體驗也不好,所以分成讀寫鎖(上)和讀寫鎖(下)兩篇。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。

7. 寫鎖獲取

rwl.writeLock().lock()

的調用

public void lock() {sync.acquire(1);}public final void acquire(int arg) { if (!tryAcquire(arg) && // 寫鎖實現了獲取鎖的方法,下文詳細講解 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 獲取鎖失敗進入同步隊列,等待被喚醒,AQS一文中重點講過 selfInterrupt();}

先分析一下可以獲取寫鎖的條件:

當前鎖的狀態1)沒有線程佔用鎖(讀寫鎖都沒被佔用) 2)線程佔用寫鎖時,線程再次來獲取寫鎖,也就是重入AQS隊列中的情況,如果是公平鎖,同步隊列中有線程等鎖時,當前線程是不可以先獲取鎖的,必須到隊列中排隊。寫鎖的標誌位只有16位,最多重入2^16-1次。

/*** ReentrantReadWriteLock.Sync.tryAcquire(int) */protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c);// 寫鎖標誌位 // 進到這個if裡,c!=0表示有線程佔用鎖 // 當有線程佔用鎖時,只有一種情況是可以獲取寫鎖的,那就是寫鎖重入 if (c != 0) { /* * 兩種情況返回false * 1.(c != 0 & w == 0) * c!=0表示標誌位!=0,w==0表示寫鎖標誌位==0,總的標誌位不為0而寫鎖標誌位(低16位)為0,只能是讀鎖標誌位(高16位)不為0 * 也就是有線程佔用讀鎖,此時不能獲取寫鎖,返回false * * 2.(c != 0 & w != 0 & current != getExclusiveOwnerThread()) * c != 0 & w != 0 表示寫鎖標誌位不為0,有線程佔用寫鎖 * current != getExclusiveOwnerThread() 佔用寫鎖的線程不是當前線程 * 不能獲取寫鎖,返回false */ if (w == 0 || current != getExclusiveOwnerThread()) return false; // 重入次數不能超過2^16-1 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); /* * 修改標誌位 * 這裡修改標誌位為什麼沒有用CAS原子操作呢? * 因為到這裡肯定是寫鎖重入了,寫鎖是獨佔鎖,不會有其他線程來搗亂。 */ setState(c + acquires); return true; } /* * 到這裡表示鎖是沒有被線程佔用的,因為鎖被線程佔用的情況在上個if裡處理並返回了 * 所以這裡直接檢查AQS隊列情況,沒問題的話CAS修改標誌位獲取鎖 */ if (writerShouldBlock() || // 檢查AQS隊列中的情況,看是當前線程是否可以獲取寫鎖 !compareAndSetState(c, c + acquires)) // 修改寫鎖標誌位 return false; setExclusiveOwnerThread(current);// 獲取寫鎖成功,將AQS.exclusiveOwnerThread置為當前線程 return true;}

簡單看下writerShouldBlock()

writerShouldBlock():檢查AQS隊列中的情況,看是當前線程是否可以獲取寫鎖,返回false表示可以獲取寫鎖。

對於公平鎖來說,如果隊列中還有線程在等鎖,就不允許新來的線程獲得鎖,必須進入隊列排隊。

hasQueuedPredecessors()方法在重入鎖的文章中分析過,判斷同步隊列中是否還有等鎖的線程,如果有其他線程等鎖,返回true當前線程不能獲取讀鎖。

// 公平鎖final boolean writerShouldBlock() {return hasQueuedPredecessors();}

對於非公平鎖來說,不需要關心隊列中的情況,有機會直接嘗試搶鎖就好了,所以直接返回false。

// 非公平鎖final boolean writerShouldBlock() {return false;}

8. 寫鎖釋放

寫鎖釋放比較簡單,跟之前的重入鎖釋放基本類似,看下源碼:

public void unlock() {sync.release(1);}/** * 釋放寫鎖,如果釋放之後沒有線程佔用寫鎖,喚醒隊列中的線程來獲取鎖 */public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);// 喚醒head的後繼節點去獲取鎖 return true; } return false;}/** * 釋放寫鎖,修改寫鎖標誌位和exclusiveOwnerThread * 如果這個寫鎖釋放之後,沒有線程佔用寫鎖了,返回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;}

9. 鎖降級

讀寫鎖支持鎖降級。鎖降級就是寫鎖是可以降級為讀鎖的,但是需要遵循獲取寫鎖、獲取讀鎖、釋放寫鎖的次序

為什麼要支持鎖降級?

支持降級鎖的情況:線程A持有寫鎖時,線程A要讀取共享數據,線程A直接獲取讀鎖讀取數據就好了。

如果不支持鎖降級會怎麼樣?

線程A持有寫鎖時,線程A要讀取共享數據,但是線程A不能獲取讀鎖,只能等待釋放寫鎖。

當線程A釋放寫鎖之後,線程A獲取讀鎖要和其他線程搶鎖,如果另一個線程B搶到了寫鎖,對數據進行了修改,那麼線程B釋放寫鎖之後,線程A才能獲取讀鎖。線程B獲取到讀鎖之後讀取的數據就不是線程A修改的數據了,也就是髒數據。

源碼中哪裡支持鎖降級?

tryAcquireShared()方法中,當前線程佔用寫鎖時是可以獲取讀鎖的,如下:

protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread(); int c = getState(); /* * 根據鎖的狀態判斷可以獲取讀鎖的情況: * 1. 讀鎖寫鎖都沒有被佔用 * 2. 只有讀鎖被佔用 * 3. 寫鎖被自己線程佔用 * 總結一下,只有在其它線程持有寫鎖時,不能獲取讀鎖,其它情況都可以去獲取。 */ if (exclusiveCount(c) != 0 && // 寫鎖被佔用 getExclusiveOwnerThread() != current) // 持有寫鎖的不是當前線程 return -1; ...

不支持鎖升級

持有寫鎖的線程,去獲取讀鎖的過程稱為鎖降級;持有讀鎖的線程,在沒釋放的情況下不能去獲取寫鎖的過程稱為鎖升級。

讀寫鎖是不支持鎖升級的。獲取寫鎖的tryAcquire()方法:

protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); /* * (c != 0 & w == 0)時返回false,不能獲取寫鎖 * c != 0 表示state不是0 * w == 0 表示寫鎖標誌位state的低16位為0 * 所以state的高16位不為0,也就是有線程佔有讀鎖 * 也就是說只要有線程佔有讀鎖返回false,不能獲取寫鎖,當然線程自己持有讀鎖時也就不能獲取寫鎖了 */ if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; ...

8. 應用

讀寫鎖多用於解決讀多寫少的問題,最典型的就是緩存問題。如下是官方給出的應用示例:

class CachedData {Object data; volatile boolean cacheValid; // 讀寫鎖實例 final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { // 獲取讀鎖 rwl.readLock().lock(); if (!cacheValid) { // 如果緩存過期了,或者為 null // 釋放掉讀鎖,然後獲取寫鎖 (後面會看到,沒釋放掉讀鎖就獲取寫鎖,會發生死鎖情況) rwl.readLock().unlock(); rwl.writeLock().lock(); try { if (!cacheValid) { // 重新判斷,因為在等待寫鎖的過程中,可能前面有其他寫線程執行過了 data = ... cacheValid = true; } // 獲取讀鎖 (持有寫鎖的情況下,是允許獲取讀鎖的,稱為 「鎖降級」,反之不行。) rwl.readLock().lock(); } finally { // 釋放寫鎖,此時還剩一個讀鎖 rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { // 釋放讀鎖 rwl.readLock().unlock(); } }}

總結

可以獲取寫鎖的情況只有兩種:

讀鎖和寫鎖都沒有線程佔用當前線程佔用寫鎖,也就寫鎖重入讀寫鎖支持鎖降級,不支持鎖升級。鎖降級就是寫鎖是可以降級為讀鎖的,但是需要遵循獲取寫鎖、獲取讀鎖、釋放寫鎖的次序。

讀寫鎖多用於解決讀多寫少的問題,最典型的就是緩存問題。

相關焦點

  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 「原創」Java並發編程系列13|LookSupport
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 13 篇,文末有本系列文章匯總。java.util.concurrent 中源碼頻繁使用的 LockSupport 來阻塞線程和喚醒線程,如 AQS 的底層實現用到 LockSupport.park()方法和 LockSupport.unpark()方法。LockSupport 到底是什麼?
  • 原創】Java並發編程系列01|開篇獲獎感言
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫為什麼要學並發編程我曾聽一個從事15年開發工作的技術人員說過,他剛工作時的並發編程第一原則就是不要寫並發程序。
  • 「原創」Java並發編程系列06|你不知道的final
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫final在Java中是一個保留的關鍵字,可以修飾變量、方法和類。那麼fianl在並發編程中有什麼作用呢?本文就在對final常見應用總結基礎上,講解final並發編程中的應用。
  • Java並發編程系列20|StampedLock源碼解析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 20 篇,文末有本系列文章匯總。寫鎖:state 第 8 位為寫鎖標誌,0 表示未被佔用,1 表示寫鎖被佔用。state 第 8-64 位表示寫鎖的獲取次數,次數超過 64 位最大容量則重新從 1 開始。樂觀讀鎖:不需要維護鎖狀態,但是在具體操作數據前要檢查一下自己操作的數據是否經過修改操作,也就是驗證是否有線程獲取過寫鎖。
  • 「原創」Java並發編程系列24|信號量Semaphore
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇介紹第三個並發工具類Semaphore,Semaphore可以理解為信號量,用於控制資源能夠被並發訪問的線程數量,以保證多個線程能夠合理的使用特定資源。
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「原創」你所不知道的讀寫鎖
    今天呢,我們來講講並發編程的讀寫鎖-ReadWriteLock那麼什麼是讀寫鎖我們知道鎖基本上是排他性的(同一時刻只允許一個線程進行訪問),而讀寫鎖在同一時刻可以允許多個線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。
  • 「原創」Java並發編程系列26|ConcurrentHashMap(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫終於輪到ConcurrentHashMap了,並發編程必備,也是面試必備。HashMap 作為使用最頻繁的集合之一,在多線程環境下是不能用的,因為 HashMap 的設計上就沒有考慮並發環境,極易導致線程安全問題。為了解決該問題,提供了 Hashtable 和 Collections.synchronizedMap(hashMap)兩種解決方案,但是這兩種方案都是對讀寫加獨佔鎖,一個線程在讀時其他線程必須等待,吞吐量和性能都較低。
  • 「零基礎學JAVA」基礎篇 第二章 JAVA編程初體驗
    JAVA【零基礎學編程】系列今天給大家帶來基礎篇 第二章 JAVA編程初體驗本節的部分編碼操作需要先安裝JDK開發工具「零基礎學JAVA」工具篇 JDK的安裝教程(WINDOWS版)「零基礎學JAVA」工具篇 環境變量的配置(WINDOWS版)
  • NO.001- 簡說 Java 並發編程史
    這篇文章是Java並發編程思想系列的第一篇,主要從理解Java並發編程歷史的原因和Java並發演進過程兩部分,以極簡地回溯並發編程的歷史,幫助大家從歷史這個角度去了解一門語言一個特性的演進。對歷史理解的越多,思考的越多,未來的方向就會更加堅定。我是誰?從哪來?到哪去?
  • Java書架來啦!入門到進階必看書籍推薦
    一直以來求Java書籍推薦的同學就很多,所以我們就整理了下Java方面推薦人數最多的幾本書,希望對大家的學習之路有一定的幫助~本書單共包括4個系列,包括Java入門系列、Java進階系列、Java Web系列、重構與設計系列的必讀書籍。這些都是大家高頻推薦書籍,讀完這些書籍,大家就會大概對Java後端有個全面的認識了。
  • 「原創」Java並發編程系列36|FutureTask
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫線程池源碼中出現了很多Callable、Future、FutureTask等以前沒介紹過的接口,尤其是線程池提交任務時總是把任務封裝成FutureTask,今天就來為大家解惑:
  • JAVA高並發網絡編程之BIO堵塞網絡編程
    上次說了網絡編程都是有作業系統統一的API的,每個語言有對它的實現,這次來一起說說通過java原生的socket編程完成BIO的網絡編程。
  • Java 編程,哪些書值得推薦?
    回到正題,現階段如果學習 Java 編程,應該讀哪些書呢 ?向左,向右Java 編程的邏輯 馬老師從 Java 的編程基礎:基本數據類型/順序/分支/循環/範型與容器/文件IO,到Java的高並發/反射/動態代理,再到 Java 8 開始支持的函數式編程
  • Alibaba架構師從零開始,一步一步帶你進入並發編程的世界
    如果你想成為一名架構師, 如果你想成為一名資深的技術大牛,強烈推薦你讀一讀, 你值得擁有!如果你是一名並發編程初學者,建議按照順序閱讀本書,並按照書中的例子進行編碼和實戰。如果你有一定的並發編程經驗,可以把本書當做一個手冊, 直接看需要學習的章節。以下是各章節的基本介紹。第l章介紹Java並發編程的挑戰,向讀者說明進入並發編程的世界可能會遇到哪些問題,以及如何解決。
  • Java並發編程系列23|循環屏障CyclicBarrier
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇介紹第二個並發工具類CyclicBarrier,CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier),分以下部分介紹:
  • Java並發編程系列21|Condition-Lock的等待通知
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫我們知道 synchronized 鎖通過 Object 類的 wait()和 notify()方法實現線程間的等待通知機制,而比 synchronized 更靈活 Lock 鎖同樣也有實現等待通知機制的方式
  • Java從零開始學 - 第34天:google提供的一些好用的並發工具類
    點擊上方關注 「Java研究所」設為「星標」,和你一起掌握更多資料庫知識java高並發系列第34篇。環境:jdk1.8。關於並發方面的,juc已幫我們提供了很多好用的工具,而谷歌在此基礎上做了擴展,使並發編程更容易,這些工具放在guava.jar包中。