李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
52.ReentrantReadWriteLock介绍
Leefs
2022-11-27 PM
1215℃
0条
[TOC] ### 前言 之前也整理过一篇关于读写锁的文章:[*《Java锁--读写锁简介》*](https://lilinchao.com/archives/640.html),现在又碰到这个话题,就在系统清晰的整理一遍,温故一下之前所学习的并发知识。 ### 一、概述 `ReentrantLock`是独占锁,某一时刻只有一个线程可以获取该锁,而实际上会存在很多**读多写少**的场景,而读操作本身并不会存在数据竞争问题,如果使用独占锁,可能会导致其中一个读线程使其他的读线程陷入等待,降低性能。 针对这种读多写少的场景,读写锁应运而生。**读写锁允许同一时刻有多个读线程访问,但在写线程访问时,所有的读线程和其他写线程均被阻塞。** **JUC包中的读写锁接口为`ReadWriteLock`**: ```java 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();//返回写锁 } ``` 读写锁其实就是维护了一对锁,一**个写锁一个读锁**,通过读写分离的策略,允许多个线程同时获取读锁,大大提高并发性。 #### 特点 - 读写锁的内部包含两把锁: + 一把是读(操作)锁,是一种共享锁; + 另一把是写(操作)锁,是一种独占锁。 - 在没有写锁的时候,读锁可以被多个线程同时持有; - 写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞,抢占读锁也会阻塞。 #### 功能 - 支持公平和非公平的获取锁的方式。 - 支持可重入:读线程在获取读锁后还可以获得读锁,写线程获取写锁后可以再次获得写锁或者读锁。 - 允许从写入锁降级为读取锁:先获取写锁,再获取读锁,最后释放写锁,不允许从读锁升级到写锁。 - 读锁和写锁都支持锁获取期间的中断。 - Condition支持:写入锁提供一个Condition实现,读取锁不支持Condition(抛出`UnsupportedOperationException`)。 ### 二、类图分析 ![52.ReentrantReadWriteLock介绍01.jpg](https://lilinchao.com/usr/uploads/2022/11/3483492267.jpg) **1. ReentrantReadWriteLock实现了ReadWritrLock接口** 源码为上方概述部分JUC包中的读写锁接口 **2. FairSync、NonfairSync继承Sync类,提供了公平和非公平的实现** ```java static final class NonfairSync extends Sync{ } static final class FairSync extends Sync { } abstract static class Sync extends AbstractQueuedSynchronizer{ } ``` **3. WriteLock、Sync、ReadLock、FairSync、NonfaruSyns都是ReadWritrLock的静态内部类** AQS 中只维护了一个 state 状态,而 `ReentrantReadWriteLock` 则需要维护读状态和写状态 ```java public abstract class AbstractQueuedSynchronizer{ private volatile int state;//state是int类型 32位 } ``` 用 state 的高16 位表示读状态,也就是获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数 。 ```java static final int SHARED_SHIFT = 16; //共享锁(读锁)状态单位值 65536 1<<16 2^16 static final int SHARED_UNIT = (1 << SHARED_SHIFT); //共享锁线程最大个数 65535 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //排它锁(写锁)掩码,二进制,15 个 1 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //返回读锁线程数 /** Returns the number of shared holds represented in count */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //返回写锁可重入个数 /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //firstReader 用来记录第一个获取到读锁的线程 private transient Thread firstReader = null; //firstReaderHoldCount 则记录第 一个获取到读锁的线程获取读锁的可重入次数 private transient int firstReaderHoldCount; //cachedHoldCounter 用来记录最后 一个获取读锁的线程获取读锁 的可重入次数 private transient HoldCounter cachedHoldCounter; ``` 该部分,后面将进行详细介绍。 **Sync类继承AQS**,它的**内部也存在两个内部类**,分别为`HoldCounter`和`ThreadLocalHoldCounter` ```java //计数器,主要与读锁配套使用 static final class HoldCounter { //计数 int count = 0; // Use id, not reference, to avoid garbage retention //获取当前线程的TID属性,该字段可以用来唯一标识一个线程 final long tid = getThreadId(Thread.currentThread()); } //本地线程计数器 static final class ThreadLocalHoldCounter extends ThreadLocal
{ // 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值 public HoldCounter initialValue() { return new HoldCounter(); } } ``` ### 三、读写状态设计 同步状态在前面重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。 读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,**高16位表示读,低16位表示写**。 ![52.ReentrantReadWriteLock介绍02.png](https://lilinchao.com/usr/uploads/2022/11/3117753236.png) 例如当前同步状态值为S,则读写状态的获取与操作如下所示 - **获取写状态** **S&0x0000FFFF**,即将高16位全部抹去 - **获取读状态** **S>>>6**:无符号补0,右移16位 - **写状态加1** **S+1** - **读状态加1** **S+(1<<16)**:即S+0x00010000 在代码层面的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。 ### 四、使用 提供一个数据容器类内部分别使用读锁保护数据的 `read()` 方法,写锁保护数据的 `write()` 方法 ```java @Slf4j(topic = "c.DataContainer") class DataContainer { private Object data; private ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock r = rw.readLock(); private ReentrantReadWriteLock.WriteLock w = rw.writeLock(); public Object read() { log.debug("获取读锁..."); r.lock(); try { log.debug("读取"); sleep(1); return data; } finally { log.debug("释放读锁..."); r.unlock(); } } public void write() { log.debug("获取写锁..."); w.lock(); try { log.debug("写入"); sleep(1); } finally { log.debug("释放写锁..."); w.unlock(); } } } ``` + **测试 `读锁-读锁` 可以并发** ```java public static void main(String[] args) { DataContainer dataContainer = new DataContainer(); new Thread(() -> { dataContainer.read(); }, "t1").start(); new Thread(() -> { dataContainer.read(); }, "t2").start(); } ``` **运行结果** ``` 12:08:41.198 [t1] DEBUG c.DataContainer - 获取读锁... 12:08:41.198 [t2] DEBUG c.DataContainer - 获取读锁... 12:08:41.200 [t1] DEBUG c.DataContainer - 读取 12:08:41.201 [t2] DEBUG c.DataContainer - 读取 12:08:42.203 [t2] DEBUG c.DataContainer - 释放读锁... 12:08:42.203 [t1] DEBUG c.DataContainer - 释放读锁... ``` 从输出结果可以看到 `t1` 线程锁定期间,`t2` 线程的读操作不受影响 + **测试 `读锁-写锁` 相互阻塞** ```java public static void main(String[] args) { DataContainer dataContainer = new DataContainer(); new Thread(() -> { dataContainer.read(); }, "t1").start(); sleep(0.1); new Thread(() -> { dataContainer.write(); }, "t2").start(); } ``` **运行结果** ``` 12:10:50.689 [t1] DEBUG c.DataContainer - 获取读锁... 12:10:50.691 [t1] DEBUG c.DataContainer - 读取 12:10:50.788 [t2] DEBUG c.DataContainer - 获取写锁... 12:10:51.692 [t1] DEBUG c.DataContainer - 释放读锁... 12:10:51.692 [t2] DEBUG c.DataContainer - 写入 12:10:52.694 [t2] DEBUG c.DataContainer - 释放写锁... ``` 从结果可以看出,当读锁被释放之后,写锁才能开始执行写入操作,即读锁和写锁产生了互斥。 + **`写锁-写锁` 也是相互阻塞的,这里就不测试了** #### 注意事项 - 读锁不支持条件变量 - 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待 ```java readLock.lock(); try { // ... writeLock.lock(); try { // ... } finally{ writeLock.unlock(); } } finally{ readLock.unlock(); } ``` - 重入时降级支持:即持有写锁的情况下去获取读锁 ```java class CachedData { Object data; // 是否有效,如果失效,需要重新计算 data volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // 获取写锁前必须释放读锁 rwl.readLock().unlock(); rwl.writeLock().lock(); try { // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新 if (!cacheValid) { data = ... cacheValid = true; } // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); } } // 自己用完数据, 释放读锁 try { use(data); } finally { rwl.readLock().unlock(); } } } ``` ### 总结 #### 读写互斥原则 > 1. 读读相容 > 2. 读写互斥 > 3. 写写互斥 #### 注意事项 1、读锁不支持条件变量 2、重入时升级不支持,即当前线程如果持有读锁,则在重入获得锁时不能在获得写锁 3、重入支持降级,即当前线程如果持有写锁,则在重入时可以获得读锁
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2630.html
上一篇
51.ReentrantLock原理
下一篇
53.ReentrantReadWriteLock应用之缓存
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
BurpSuite
Spring
二叉树
持有对象
SpringCloudAlibaba
Elastisearch
Git
NIO
RSA加解密
链表
序列化和反序列化
Ubuntu
随笔
Map
SQL练习题
Typora
Netty
微服务
散列
Spark SQL
GET和POST
CentOS
Java编程思想
MyBatis
数学
JVM
gorm
栈
Golang
Quartz
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭