深入理解读写锁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相关推荐

  1. 读写锁ReentrantReadWriteLock源码分析

    文章目录 读写锁的介绍 写锁详解 写锁的获取 写锁的释放 读锁详解 读锁的获取 读锁的释放 锁降级 读写锁的介绍 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java ...

  2. Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析

    文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...

  3. 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition

    文章目录 J.U.C脑图 ReentrantLock概述 ReentrantLock 常用方法 synchronized 和 ReentrantLock的比较 ReentrantLock示例 读写锁R ...

  4. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  5. 深入分析实战可重入读写锁ReentrantReadWriteLock

    文章目录 前言 加锁规则 同步原理 源码解析 实战演示 前言 前面我们学习了可重入锁ReentrantLock,可重入锁是一个排他锁,只要不是当前线程访问加锁资源都不能够进入,只能等待锁的释放.当然, ...

  6. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    JDK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁,一个读锁,一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下,读写 ...

  7. Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析

    Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析 1.[原文链接](https://docs.oracle.com/javase/8/docs/api/ja ...

  8. 读写锁 -- ReentrantReadWriteLock

    1. 类继承关系 如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成. public interface ReadWriteLock {Lock readLock();L ...

  9. 可重入读写锁ReentrantReadWriteLock的使用详解

    ReentrantReadWriteLock是一把可重入读写锁,这篇文章主要是从使用的角度帮你理解,希望对你有帮助. 一.性质 1.可重入 如果你了解过synchronized关键字,一定知道他的可重 ...

最新文章

  1. [JSP][JSTL]页面调用函数--它${fn:}内置函数、是推断字符串是空的、更换车厢
  2. 【OpenGL】用OpenGL shader实现将YUV(YUV420,YV12)转RGB-(直接调用GPU实现,纯硬件方式,效率高)...
  3. Make it run, make it right, make it fast
  4. mysql 表名和和数据库函数名称冲突的解决方法
  5. 网易云信携手房天下打造高质量音视频会议
  6. UVA 413|LA 5388|POJ 1492|ZOJ 1338 Up and Down Sequences
  7. PHP foreach 小结
  8. 微信小程序的一些数据调用方式
  9. 【codevs2952】 细胞分裂2,快速幂模版
  10. java long类型判断_Java中的long类型和Long类型比较大小
  11. u-boot编译连接分析
  12. AlertDialog源码解析之一
  13. 成立两年估值17亿美元,这家Hinton点赞的AI芯片公司获宝马微软投资
  14. poj2182-Lost Cow
  15. sps的process插件安装包_什么是Process插件?在中介和调节效应分析中有哪些优势和不足?如何下载与安装?...
  16. 基于ros单线激光雷达的坐标读取
  17. 解决启动eureka报错Unable to start web ... nested exception is org.springframework.boot.web.server.WebS
  18. pdf文件展示盖章及下载
  19. Go语言操作sqllite
  20. Linux与git使用引导(git rm 与rm命令)

热门文章

  1. 甲子光年推出中国低代码行业分析报告:本地私有化部署占比超过一半
  2. TikTok三大流行趋势 钛动带你看懂TikTok
  3. win10新建虚拟机网络配置未连接服务器,Win10系统VMWare虚拟机无法连接网络怎么办?...
  4. Excel学习笔记之基础篇
  5. 什么是SEM?SEM是否包括SEO?
  6. 如何启动一个ATX电源
  7. 基于Web开发资料专集
  8. java计算机毕业设计ssm基于SSM学生信息管理系统37myx(附源码、数据库)
  9. 一款免费的Veracrypt加密软件---U盘加密功能
  10. 火狐开发----Web开发者工具