JUC系列(八)| 读写锁-ReadWriteLock
多線程一直Java開發中的難點,也是面試中的常客,趁著還有時間,打算鞏固一下JUC方面知識,我想機會隨處可見,但始終都是留給有準備的人的,希望我們都能加油!!!
沉下去,再浮上來,我想我們會變的不一樣的。
一個非常喜歡的女孩子拍的照片
作者:次辣條嗎
一、讀寫鎖
1)概述:
我們開發中應該能夠遇到這樣的一種情況,對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是當一個寫者線程在寫這些共享資源時,就不允許其他線程進行訪問。
針對這種場景,Java的并發包下提供了讀寫鎖 ReadWriteLock(接口) | ReentrantReadWriteLock(實現類)。
讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。我們將讀操作相關的鎖,稱為讀鎖,因為可以共享讀,我們也稱為“共享鎖”,將寫操作相關的鎖,稱為寫鎖、排他鎖、獨占鎖。每次可以多個線程的讀者進行讀訪問,但是一次只能由一個寫者線程進行寫操作,即寫操作是獨占式的。
讀寫鎖適合于對數據結構的讀次數比寫次數多得多的情況. 因為, 讀模式鎖定時可以共享, 以寫模式鎖住時意味著獨占, 所以讀寫鎖又叫共享-獨占鎖。
public interface ReadWriteLock {// 讀鎖Lock readLock();// 寫鎖Lock writeLock(); }ReentrantReadWriteLock這個得自己去看哈,這里給出一個整體架構哈😁。
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {/** 讀鎖 */private final ReentrantReadWriteLock.ReadLock readerLock;/** 寫鎖 */private final ReentrantReadWriteLock.WriteLock writerLock;final Sync sync;/** 使用默認(非公平)的排序屬性創建一個新的ReentrantReadWriteLock */public ReentrantReadWriteLock() {this(false);}/** 使用給定的公平策略創建一個新的 ReentrantReadWriteLock */public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}/** 返回用于寫入操作的鎖 */public ReentrantReadWriteLock.WriteLock writeLock() { returnwriterLock; }/** 返回用于讀取操作的鎖 */public ReentrantReadWriteLock.ReadLock readLock() { returnreaderLock; }abstract static class Sync extends AbstractQueuedSynchronizer {}static final class NonfairSync extends Sync {}static final class FairSync extends Sync {}public static class ReadLock implements Lock, java.io.Serializable {}public static class WriteLock implements Lock, java.io.Serializable {} }2)使用相關:
當讀寫鎖是寫加鎖狀態時, 在這個鎖被解鎖之前, 所有試圖對這個鎖加鎖的線程都會被阻塞.
當讀寫鎖在讀加鎖狀態時, 所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權, 但是如果線程希望以寫模式對此鎖進行加鎖, 它必須直到所有的線程釋放鎖.
如果線程想要進入讀鎖的前提條件:
-
不存在其他線程的寫鎖
-
沒有寫請求, 或者有寫請求,但調用線程和持有鎖的線程是同一個(可重入鎖)
線程進入寫鎖的前提條件:
- 沒有讀者線程正在訪問
- 沒有其他寫者線程正在訪問
通常, 當讀寫鎖處于讀模式鎖住狀態時, 如果有另外線程試圖以寫模式加鎖, 讀寫鎖通常會阻塞隨后的讀模式鎖請求, 這樣可以避免讀模式鎖長期占用, 而等待的寫模式鎖請求長期阻塞.
3)特點:
🛫公平選擇性:
非公平模式(默認)
- 當以非公平初始化時,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取,可能會延緩一個或多個讀或寫線程,但是會比公平鎖有更高的吞吐量。
公平模式
-
當以公平模式初始化時,線程將會以隊列的順序獲取鎖。當當前線程釋放鎖后,等待時間最長的寫鎖線程就會被分配寫鎖;或者有一組讀線程組等待時間比寫線程長,那么這組讀線程組將會被分配讀鎖。
-
當有寫線程持有寫鎖或者有等待的寫線程時,一個嘗試獲取公平的讀鎖(非重入)的線程就會阻塞。這個線程直到等待時間最長的寫鎖獲得鎖后并釋放掉鎖后才能獲取到讀鎖。
🛬可重入
讀鎖和寫鎖都支持線程重進入。但是寫鎖可以獲得讀鎖,讀鎖不能獲得寫鎖。因為讀鎖是共享的,寫鎖是獨占式的。
💺鎖降級
遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為 讀鎖。
🚤支持中斷鎖的獲取
在讀鎖和寫鎖的獲取過程中支持中斷
🛸監控
提供一些輔助方法,例如hasQueuedThreads方法查詢是否有線程正在等待獲取讀鎖或寫鎖、isWriteLocked方法查詢寫鎖是否被任何線程持有等等
二、案例實現
一個特別簡單的案例哈。
🍟代碼
場景: 使用 ReentrantReadWriteLock 對一個 hashmap 進行讀和寫操作
package com.crush.juc06;import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;//資源類 class ReentrantReadWriteLockDemo{//創建 map 集合private volatile Map<String, Object> map = new HashMap<>();//創建讀寫鎖對象private ReadWriteLock rwLock = new ReentrantReadWriteLock();//放數據public void put(String key, Object value) {//添加寫鎖rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在讀數據" + key);//暫停一會TimeUnit.MICROSECONDS.sleep(300);//放數據map.put(key, value);System.out.println(Thread.currentThread().getName() + "讀完了" + key);} catch (InterruptedException e) {e.printStackTrace();} finally {//釋放寫鎖rwLock.writeLock().unlock();}}//取數據public Object get(String key) {//添加讀鎖rwLock.readLock().lock();Object result = null;try {System.out.println(Thread.currentThread().getName() + "正在取數據" + key);//暫停一會TimeUnit.MICROSECONDS.sleep(300);result = map.get(key);System.out.println(Thread.currentThread().getName() + "取完數據了" + key);} catch (InterruptedException e) {e.printStackTrace();} finally {//釋放讀鎖rwLock.readLock().unlock();}return result;}public static void main(String[] args) {ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();for (int i = 1; i <= 5; i++) {final int number = i;new Thread(() -> {demo.put(String.valueOf(number), number);}, String.valueOf(i)).start();}for (int i = 1; i <= 5; i++) {final int number = i;new Thread(() -> {demo.get(String.valueOf(number));}, String.valueOf(i)).start();}} } /** 5正在進行寫操作5 5寫完了5 4正在進行寫操作4 4寫完了4 3正在進行寫操作3 3寫完了3 2正在進行寫操作2 2寫完了2 1正在進行寫操作1 1寫完了1 1正在取數據1 4正在取數據4 3正在取數據3 5正在取數據5 2正在取數據2 1取完數據了1 4取完數據了4 2取完數據了2 5取完數據了5 3取完數據了3*/寫是唯一的,而讀的時候是共享的。
🍔小總結
ReentrantReadWriteLock和Synchonized、ReentrantLock比較起來有哪些區別呢?或者有哪些優勢呢?
-
Synchonized、ReentrantLock是屬于獨占鎖,讀、寫操作每次都只能是一個人訪問,效率比較低。
-
而ReentrantReadWriteLock讀操作可以共享,提升性能,允許多人一起讀操作,而寫操作還是每次一個人訪問。
當然ReentrantReadWriteLock優勢是有,但是也存在一些缺陷,容易造成鎖饑餓,因為如果是讀線程先拿到鎖的話,并且后續有很多讀線程,但只有一個寫線程,很有可能這個寫線程拿不到鎖,它可能要等到所有讀線程讀完才能進入,就可能會造成一種一直讀,沒有寫的現象。
三、鎖降級
🍜概念:
鎖降級的意思就是寫鎖降級為讀鎖。而讀鎖是不可以升級為寫鎖的。如果當前線程擁有寫鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程,最后釋放讀鎖的過程。
編程模型:
- 獲取寫鎖—>獲取讀鎖—>釋放寫鎖—>釋放讀鎖
簡單的代碼:
/*** @Author: crush* @Date: 2021-08-21 9:04* version 1.0*/ public class ReadWriteLockDemo2 {public static void main(String[] args) {ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();// 獲取讀鎖ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();// 獲取寫鎖ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();//1、 獲取到寫鎖writeLock.lock();System.out.println("獲取到了寫鎖");//2、 繼續獲取到寫鎖readLock.lock();System.out.println("繼續獲取到讀鎖");//3、釋放寫鎖writeLock.unlock();//4、 釋放讀鎖readLock.unlock();} } /*** 獲取到了寫鎖* 繼續獲取到讀鎖*/也許大家覺得看不出什么,但是如果將獲取讀鎖那一行代碼調到獲取寫鎖上方去,可能結果就完全不一樣拉。
public class ReadWriteLockDemo2 {public static void main(String[] args) {ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();// 獲取讀鎖ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();// 獲取寫鎖ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();//1、 獲取到讀鎖readLock.lock();System.out.println("獲取到了讀鎖");writeLock.lock();System.out.println("繼續獲取到寫鎖");writeLock.unlock();readLock.unlock();// 釋放寫鎖} }🍿原因:
為什么會出現上面這一幕呢?
- 原因: 當線程獲取讀鎖的時候,可能有其他線程同時也在持有讀鎖,因此不能把獲取讀鎖的線程“升級”為寫鎖;而對于獲得寫鎖的線程,它一定獨占了讀寫鎖,因此可以繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖后,還可以先釋放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”為了讀鎖。
上面就一普通案例,看完確實會有點迷,這只是做個簡單證明,下面才是正文哈。😁
🌭使用場景:
對于數據比較敏感, 需要在對數據修改以后, 獲取到修改后的值, 并進行接下來的其它操作
我們來看個比較實在的案例:
import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;public class CacheDemo {/*** 緩存器,這里假設需要存儲1000左右個緩存對象,按照默認的負載因子0.75,則容量=750,大概估計每一個節點鏈表長度為5個* 那么數組長度大概為:150,又有雨設置map大小一般為2的指數,則最近的數字為:128*/private Map<String, Object> map = new HashMap<>(128);private ReadWriteLock rwl = new ReentrantReadWriteLock();private Lock writeLock=rwl.writeLock();private Lock readLock=rwl.readLock();public static void main(String[] args) {}public Object get(String id) {Object value = null;readLock.lock();//首先開啟讀鎖,從緩存中去取try {//如果緩存中沒有 釋放讀鎖,上寫鎖if (map.get(id) == null) { readLock.unlock();writeLock.lock();try {//防止多寫線程重復查詢賦值if (value == null) {//此時可以去數據庫中查找,這里簡單的模擬一下value = "redis-value"; }//加讀鎖降級寫鎖,不明白的可以查看上面鎖降級的原理與保持讀取數據原子性的講解readLock.lock(); } finally {//釋放寫鎖writeLock.unlock(); }}} finally {//最后釋放讀鎖readLock.unlock(); }return value;} }如果不使用鎖降級功能,如先釋放寫鎖,然后獲得讀鎖,在這個獲取讀鎖的過程中,可能會有其他線程競爭到寫鎖 或者是更新數據 則獲得的數據是其他線程更新的數據,可能會造成數據的污染,即產生臟讀的問題。
🍖鎖降級的必要性:
鎖降級中讀鎖的獲取是否必要呢?
答案是必要的。主要是為了保證數據的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖, 假設此刻另一個線程(記作線程T)獲取了寫鎖并修改了數據,那么當前線程無法感知線程T的數據更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據并釋放讀鎖之后,線程T才能獲取寫鎖進行數據更新。
四、自言自語
最近又開始了JUC的學習,感覺Java內容真的很多,但是為了能夠走的更遠,還是覺得應該需要打牢一下基礎。
最近在持續更新中,如果你覺得對你有所幫助,也感興趣的話,關注我吧,讓我們一起學習,一起討論吧。
你好,我是博主寧在春,Java學習路上的一顆小小的種子,也希望有一天能扎根長成蒼天大樹。
希望與君共勉😁
我們:待別時相見時,都已有所成。
參考:
并發庫應用之五 & ReadWriteLock場景應用
讀寫鎖的使用場景及鎖降級
深入理解讀寫鎖—ReadWriteLock源碼分析
總結
以上是生活随笔為你收集整理的JUC系列(八)| 读写锁-ReadWriteLock的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java设计模式-工厂模式(3)抽象工厂
- 下一篇: 史上最详细微信小程序授权登录与后端Spr