深入理解读写锁ReentrantReadWriteLock
深入理解读写锁ReentrantReadWriteLock
前言
业务开发中我们可能涉及到读写操作。
面对写和读,对于数据同步,在使用Lock
锁和 synchronized
关键字同步数据时候,对于读读而言,两个线程也需要争抢锁,此时额外争抢锁是没有意义的,造成性能损耗,写的时候,不能读,没有写的时候,读线程不能互斥。
对于 Lock
锁和 synchronized
来说。都是互斥锁,读读也存在互斥。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁
如实例
:
生产者和消费者而言
当一个线程负责生产,2个线程负责消费,生产者没有进行生产时,两个消费线程都可以去消费数据(这里我们不考虑 重复数据问题)
两个线程彼此还要争抢资源
private static final int LINED_SIZE = 1000;private static int num = 0;private static final Object lock = new Object();private static final LinkedList<Integer> linkedList = new LinkedList<>();public static void main(String[] args) throws InterruptedException {t1.start();t2.start();t3.start();t1.setPriority(5);t2.setPriority(5);t3.setPriority(5);t1.join();t2.join();t3.join();TimeUnit.SECONDS.sleep(2);System.out.println(" main end ");}static class ConsumerObje implements Runnable {@Overridepublic void run() {while (true){synchronized (lock) {while (linkedList.size() == 0) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}try {Thread.sleep(5_00);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " : " + linkedList.removeFirst());lock.notifyAll();}}}}static class ProductObje implements Runnable {@Overridepublic void run() {while (true) {synchronized (lock) {while (linkedList.size() >= LINED_SIZE) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}int n = num++;System.out.println(" 正在生产: " + n);linkedList.addLast(n);lock.notifyAll();}}}}
思考
为什么需要使用读写锁ReentrantReadWriteLock
这个锁有什么好处?缺点?
好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。
读写锁ReentrantReadWriteLock用法详解
ReentrantReadWriteLock 也提供了公平和非公平锁
基于构造默认非公平锁, ReentrantReadWriteLock 读写锁内部也是基于AQS队列实现的。
public ReentrantReadWriteLock() {this(false);}
//读写锁private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);//写锁private final static Lock writeLock = readWriteLock.writeLock();//读锁private final static Lock readLock = readWriteLock.readLock();private final static List<Long> longs = new ArrayList<>();public final static void main(String[] args) throws InterruptedException {// new Thread(ReentrantReadWriteLockTest::write).start();
// TimeUnit.SECONDS.sleep(1);
// new Thread(ReentrantReadWriteLockTest::write).start();new Thread(ReentrantReadWriteLockTest::write).start();TimeUnit.SECONDS.sleep(1);new Thread(ReentrantReadWriteLockTest::read).start();new Thread(ReentrantReadWriteLockTest::read).start();}static void write() {try {writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + " write ");longs.add(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock();}}static void read() {try {readLock.lock();TimeUnit.SECONDS.sleep(1);longs.forEach(x -> System.out.println(x));} catch (InterruptedException e) {e.printStackTrace();} finally {readLock.lock();}}
可以看到我们写了一条数据,两条数据同时打印出来,读读是不互斥的。
Thread-0 write
1648997092090
1648997092090
读写锁 存在一个问题:
当读锁比例很多,写锁很少,锁竞争情况下,写锁抢到锁的机会就回少,读锁数量太大的情况下,写锁不一定能抢到锁.
我们使用非公平锁,来测试,启动5个读锁,一个写锁。
//读写锁private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//写锁private final static Lock writeLock = readWriteLock.writeLock();//读锁private final static Lock readLock = readWriteLock.readLock();private final static List<Long> longs = new ArrayList<>();public final static void main(String[] args) throws InterruptedException {new Thread(ReentrantReadWriteLockTest2::write).start();TimeUnit.SECONDS.sleep(1);//new Thread(ReentrantReadWriteLockTest2::read).start();//new Thread(ReentrantReadWriteLockTest2::read).start();for (int i = 0; i <5; i++) {new Thread(ReentrantReadWriteLockTest2::read).start();}}static void write() {for (;;){try {writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + " write ");longs.add(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock();}}}static void read() {for (;;){try {readLock.lock();TimeUnit.SECONDS.sleep(1);longs.forEach(x -> System.out.println(x));} catch (InterruptedException e) {e.printStackTrace();} finally {readLock.lock();}}}
测试结果这里就不写了,刚开始一直写,后来一直读,写锁机会很少,当读线程比例再大时,写的机会就更少了。
ReentrantReadWriteLock 原理剖析
ReentrantReadWriteLock 支持读锁和写锁
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁, 一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。
实现了读写锁的接口
既然支持读写锁,那读锁和写锁都需要一个状态去保存锁的状态,在 aqs 中是使用变量state变量进行保存的。
ReentrantReadWriteLock 中是如何保存的呢?
在源码里可以看到
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁 被一个线程重复获取的次数。
但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写 锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将 其切分为两部分:高16为表示读,低16为表示写。( 类似 线程池的状态和工作线程数量)
获取锁方法可以看到,获取锁然后再根据这个计算线程数量 ,这个方法是是写锁释放锁
状态计算
通过位运算。假如当前同步状态为S, 那么: 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。
当写状态加1,等于S+1. 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。
当读状态加1,等于 S+(1<<16),也就是S+0x00010000 。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状 态(S>>>16)大于0,即读锁已被获取。
exclusiveCount(int c) 表示获得持有写状态的锁的次数。
sharedCount(int c) 表示获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个 线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器,实际上是 ThreadLocal 保存每一个读线程锁重入的次数。
HoldCounter 计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器 的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后 才能对共享锁进行释放、重入操作。
HoldCounter是用来记录读锁重入数的对象 ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁
的线 程的其他线程的读锁重入数对象
第一个获取读锁的重入次数 可以看到 用一个变量保存 ,以及保存线程。
写锁
加锁
acquire(int arg) 方法 ,可以看到这里获取锁失败,还是加入到同步队列里,还是aqs里的方法 ,排它锁的node节点 很熟悉。
acquireQueued 方法里就不用看了,也是aqs里的设置唤醒节点为-1状态,然后unpark 阻塞。
可以看到写锁是一个支持重进入的排它锁。
如果当前线程已经获取了写锁,则增加写状态。
如果当 前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。如上面的aqs 同步队列以及unpark阻塞那段代码。
解锁
解锁很简单,和之前解锁几乎一样,唯一不一样的地方就是从高位获取锁。
读锁
加锁
通过重写AQS的tryAcquireShared方法和 tryReleaseShared方法。
看代码意思大致是
首先这里是获取写锁 然后判断是否是当前线程,这里是读写锁降级。获取读锁失败返回
下面获取读锁,这里就简单了一看就能看到,先判断第一个读锁线程以及数量, 是则设置第一个,没有就设置 1 有就+1。
不是则从 ThreadLocal 里获取设置,没有就设置 1 有就+1。
这里是读锁是否阻塞,公平锁和非公平锁的实现。可以不用管。
compareAndSetState(c, c + SHARED_UNIT) cas这行失败则表示有竞争,则执行下面代码,进行自旋重试。
解锁
这里没啥可说的,如果是第一个读锁则设置线程是null,重入次数-1
不是则从 ThreadLocal 里拿,然后进行设置以及-1。
最后cas更新读锁状态数量。
释放锁
doReleaseShared
也是共享锁的释放逻辑,还是aqs里的逻辑,很熟悉。
最后
读写锁的实现继承图
ReentrantReadWriteLock 读写锁既有有点也有缺点
好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。
使用时候需要控制读写比例,防止出现锁饥饿现象。
当出现读比例特别大时候,ReentrantReadWriteLock锁就不适合了,此时JDK8之后提供的StampedLock锁更适合读写比例大的场景
设计的精髓的地方
- 1 一个变量保存 2 个状态 和 线程池里类似
- 2 读锁的可重入使用 ThreadLocal 进行存储
- 3 写锁可以重入
- 4 写锁降级(没释放锁时候获取读锁,保证数据的一致性)
深入理解读写锁ReentrantReadWriteLock相关推荐
- 读写锁ReentrantReadWriteLock源码分析
文章目录 读写锁的介绍 写锁详解 写锁的获取 写锁的释放 读锁详解 读锁的获取 读锁的释放 锁降级 读写锁的介绍 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java ...
- Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析
文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...
- 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition
文章目录 J.U.C脑图 ReentrantLock概述 ReentrantLock 常用方法 synchronized 和 ReentrantLock的比较 ReentrantLock示例 读写锁R ...
- java 可重入读写锁 ReentrantReadWriteLock 详解
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...
- 深入分析实战可重入读写锁ReentrantReadWriteLock
文章目录 前言 加锁规则 同步原理 源码解析 实战演示 前言 前面我们学习了可重入锁ReentrantLock,可重入锁是一个排他锁,只要不是当前线程访问加锁资源都不能够进入,只能等待锁的释放.当然, ...
- 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥
JDK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁,一个读锁,一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下,读写 ...
- Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析
Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析 1.[原文链接](https://docs.oracle.com/javase/8/docs/api/ja ...
- 读写锁 -- ReentrantReadWriteLock
1. 类继承关系 如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成. public interface ReadWriteLock {Lock readLock();L ...
- 可重入读写锁ReentrantReadWriteLock的使用详解
ReentrantReadWriteLock是一把可重入读写锁,这篇文章主要是从使用的角度帮你理解,希望对你有帮助. 一.性质 1.可重入 如果你了解过synchronized关键字,一定知道他的可重 ...
最新文章
- [JSP][JSTL]页面调用函数--它${fn:}内置函数、是推断字符串是空的、更换车厢
- 【OpenGL】用OpenGL shader实现将YUV(YUV420,YV12)转RGB-(直接调用GPU实现,纯硬件方式,效率高)...
- Make it run, make it right, make it fast
- mysql 表名和和数据库函数名称冲突的解决方法
- 网易云信携手房天下打造高质量音视频会议
- UVA 413|LA 5388|POJ 1492|ZOJ 1338 Up and Down Sequences
- PHP foreach 小结
- 微信小程序的一些数据调用方式
- 【codevs2952】 细胞分裂2,快速幂模版
- java long类型判断_Java中的long类型和Long类型比较大小
- u-boot编译连接分析
- AlertDialog源码解析之一
- 成立两年估值17亿美元,这家Hinton点赞的AI芯片公司获宝马微软投资
- poj2182-Lost Cow
- sps的process插件安装包_什么是Process插件?在中介和调节效应分析中有哪些优势和不足?如何下载与安装?...
- 基于ros单线激光雷达的坐标读取
- 解决启动eureka报错Unable to start web ... nested exception is org.springframework.boot.web.server.WebS
- pdf文件展示盖章及下载
- Go语言操作sqllite
- Linux与git使用引导(git rm 与rm命令)