Java 讀寫鎖 ReadWriteLock 原理與應用場景詳解
Java併發程式設計提供了讀寫鎖,主要用於讀多寫少的場景,今天我就重點來講解讀寫鎖的底層實現原理@
什麼是讀寫鎖?
讀寫鎖並不是JAVA所特有的讀寫鎖(Readers-Writer Lock)顧名思義是一把鎖分為兩部分:讀鎖和寫鎖,其中讀鎖允許多個執行緒同時獲得,因為讀操作本身是執行緒安全的,而寫鎖則是互斥鎖,不允許多個執行緒同時獲得寫鎖,並且寫操作和讀操作也是互斥的。
所謂的讀寫鎖(Readers-Writer Lock),顧名思義就是將一個鎖拆分為讀鎖和寫鎖兩個鎖。
其中讀鎖允許多個執行緒同時獲得,而寫鎖則是互斥鎖,不允許多個執行緒同時獲得寫鎖,並且寫操作和讀操作也是互斥的。
為什麼需要讀寫鎖?
和 ReentrantLock 都是獨佔鎖,即在同一時刻只有一個執行緒獲取到鎖。
然而在有些業務場景中,我們大多在讀取資料,很少寫入資料,這種情況下,如果仍使用獨佔鎖,效率將及其低下。
針對這種情況,Java提供了讀寫鎖——ReentrantReadWriteLock。
主要解決:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁的場景。
讀寫鎖的特點
- 公平性:讀寫鎖支援非公平和公平的鎖獲取方式,非公平鎖的吞吐量優於公平鎖的吞吐量,預設構造的是非公平鎖
- 可重入:線上程獲取讀鎖之後能夠再次獲取讀鎖,但是不能獲取寫鎖,而執行緒在獲取寫鎖之後能夠再次獲取寫鎖,同時也能獲取讀鎖
- 鎖降級:執行緒獲取寫鎖之後獲取讀鎖,再釋放寫鎖,這樣實現了寫鎖變為讀鎖,也叫鎖降級
讀寫鎖的使用場景
ReentrantReadWriteLock適合讀多寫少的場景:
讀鎖ReentrantReadWriteLock.ReadLock可以被多個執行緒同時持有, 所以併發能力很高。
寫鎖ReentrantReadWriteLock.WriteLock是獨佔鎖, 在一個執行緒持有寫鎖時候, 其他執行緒都不能在搶佔, 包含搶佔讀鎖都會阻塞。
ReentrantReadWriteLock的使用場景總結:其實就是 讀讀併發、讀寫互斥、寫寫互斥而已,如果一個物件併發讀的場景大於併發寫的場景,那就可以使用 ReentrantReadWriteLock來達到保證執行緒安全的前提下提高併發效率。
讀寫鎖的主要成員和結構圖
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:讀鎖,實現了Lock介面
- WriterLock:寫鎖,也實現了Lock介面
- Sync:繼承自AbstractQueuedSynchronize(AQS),可以為公平鎖FairSync 或 非公平鎖NonfairSync
3.ReentrantReadWriteLock的成員變數和建構函式
/** 內部提供的讀鎖 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 內部提供的寫鎖 */ private final ReentrantReadWriteLock.WriteLock writerLock; /** AQS來實現的同步器 */ final Sync sync; /** * Creates a new {@code ReentrantReadWriteLock} with * 預設建立非公平的讀寫鎖 */ 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); }
讀寫鎖的實現原理
ReentrantReadWriteLock實現關鍵點,主要包括:
- 讀寫狀態的設計
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級
1.讀寫狀態的設計
之前談ReentrantLock的時候,Sync類是繼承於AQS,主要以int state為執行緒鎖狀態,0表示沒有被執行緒佔用,1表示已經有執行緒佔用。
同樣ReentrantReadWriteLock也是繼承於AQS來實現同步,那int state怎樣同時來區分讀鎖和寫鎖的?
如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,ReentrantReadWriteLock將int型別的state將變數切割成兩部分:
- 高16位記錄讀鎖狀態
- 低16位記錄寫鎖狀態
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); // 讀鎖最大數量 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 寫鎖最大數量 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 本地執行緒計數器 private transient ThreadLocalHoldCounter readHolds; // 快取的計數器 private transient HoldCounter cachedHoldCounter; // 第一個讀執行緒 private transient Thread firstReader = null; // 第一個讀執行緒的計數 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(); //獲取獨佔鎖(寫鎖)的被獲取的數量 int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) //1.如果同步狀態不為0,且寫狀態為0,則表示當前同步狀態被讀鎖獲取 //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是獲取當前鎖狀態,w是獲取寫鎖的狀態。
2)如果鎖狀態不為零,而寫鎖的狀態為0,則表示讀鎖狀態不為0,所以當前執行緒不能獲取寫鎖。或者鎖狀態不為零,而寫鎖的狀態也不為0,但是獲取寫鎖的執行緒不是當前執行緒,則當前執行緒不能獲取寫鎖。
3)寫鎖是一個可重入的排它鎖,在獲取同步狀態時,增加了一個讀鎖是否存在的判斷。
寫鎖的釋放與ReentrantLock的釋放過程類似,每次釋放將寫狀態減1,直到寫狀態為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)在沒有寫狀態為0時,讀鎖總會被成功獲取,而所做的也只是增加讀狀態(執行緒安全)
3)讀狀態是所有執行緒獲取讀鎖次數的總和,而每個執行緒各自獲取讀鎖的次數只能選擇儲存在ThreadLocal中,由執行緒自身維護。
讀鎖的每次釋放均減小狀態(執行緒安全的,可能有多個讀執行緒同時釋放鎖),減小的值是1<<16。
4.鎖降級
降級是指當前把持住寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
鎖降級過程中的讀鎖的獲取是否有必要,答案是必要的。主要是為了保證資料的可見性,如果當前執行緒不獲取讀鎖而直接釋放寫鎖,假設此刻另一個執行緒獲取的寫鎖,並修改了資料,那麼當前執行緒就步伐感知到執行緒T的資料更新,如果當前執行緒遵循鎖降級的步驟,那麼執行緒T將會被阻塞,直到當前執行緒使資料並釋放讀鎖之後,執行緒T才能獲取寫鎖進行資料更新。
5.讀鎖與寫鎖的整體流程
讀寫鎖總結
本篇詳細介紹了ReentrantReadWriteLock的特徵、實現、鎖的獲取過程,透過4個關鍵點的核心設計:
- 讀寫狀態的設計
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 鎖降級
從而才能實現:共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁的應用場景。
作者簡介
陳睿| ,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,專注於網際網路架構技術。
閱讀mikechen的網際網路架構更多技術文章合集
| | | | | |
關注「mikechen 的網際網路架構」公眾號,回覆 【架構】領取 《Java進階架構思維導圖&Java進階架構文章合集》
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70011997/viewspace-2916864/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java讀寫鎖ReadWriteLockJava
- Java反射詳解:入門+使用+原理+應用場景Java反射
- Zookeeper基礎原理&應用場景詳解
- 詳解ASR語音轉寫場景下的應用
- 面試官:你說說互斥鎖、自旋鎖、讀寫鎖、悲觀鎖、樂觀鎖的應用場景面試
- 解鎖「SOAR」在不同場景下的應用與實踐
- AI助手:Agent工作流程與應用場景詳解AI
- 讀者寫者與生產者消費者應用場景
- 機器學習 | 詳解GBDT在分類場景中的應用原理與公式推導機器學習公式
- Flink基本原理與應用場景
- FIBOS DAPP 應用場景詳解APP
- Redis詳解以及Redis的應用場景Redis
- 深刨顯式鎖ReentrantLock原理及其與內建鎖的區別,以及讀寫鎖ReentrantReadWriteLock使用場景ReentrantLock
- 意向共享鎖與意向排它鎖:詳解與應用
- ZooKeeper核心原理及應用場景
- 堆排序原理及其應用場景排序
- Nginx主要應用場景(必讀)Nginx
- 詳解 Serverless 架構的 6 大應用場景Server架構
- PHP 觀察者模式應用場景例項詳解PHP模式
- 基於Serverless架構最新應用場景詳解Server架構
- Golang 讀寫鎖RWMutex 互斥鎖Mutex 原始碼詳解GolangMutex原始碼
- Java中的鎖原理、鎖優化、CAS、AQS詳解!Java優化AQS
- BFC的概念與應用場景
- Java多執行緒的悲觀鎖與樂觀鎖及各自適用場景Java執行緒
- 線段樹詳解 (原理,實現與應用)
- Java使用讀寫鎖替代同步鎖Java
- Zookeeper應用場景彙總(超詳細)
- PHP檔案讀寫鎖的問題詳解PHP
- Hive簡介、應用場景及架構原理Hive架構
- 海外IP池的工作原理及應用場景
- Java動態代理—框架中的應用場景和基本原理Java框架
- Java動態代理——框架中的應用場景和基本原理Java框架
- Java中的讀/寫鎖Java
- @Transactional詳解(作用、失效場景與解決方法)
- 例項詳解 Java 死鎖與破解死鎖Java
- PyFlink 最新進展解讀及典型應用場景介紹
- Java鎖詳解Java
- 全面瞭解 Nginx 主要應用場景Nginx