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