一、概述:

  Java纪年1.5年,ReentrantReadWriteLock诞生于JUC,此后,国人一般称它为读写锁。人如其名,他就是一个可重入锁,同时他还是一个读写锁

  a)跟ReentrantLock并没有任何的亲属关系

  因为ReentrantReadWriteLock在命名上跟ReentrantLock非常贴近,很容易让人认为他跟ReentrantLock有继承关系,其实并没有。ReentrantReadWriteLock 实现了 ReadWriteLock 和 Serializable,同时 ReadWriteLock 跟 Lock 也没有继承关系

  ReadWriteLock 是独立的一个接口,维护了一对相关的,一个用于只读操作,另一个用于写入操作,只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

  ReentrantReadWriteLock 跟 ReentrantLock 只有朋友关系,他们都是 可重入锁

  但是ReentrantReadWriteLock 的重入递归层级只有 65535,即读锁能递归65535、写锁也同样能够递归65535层,至于为何是65535呢?在AQS框架的时候说过,AQS是用一个Integer来表示锁的状态。而一个Integer有32位,读锁用一半,写锁用一半,16bit = 65535

  

  b)ReentrantReadWriteLock也有公平性

  ReentrantReadWriteLock除了和ReentrantLock一样具有可重入性之外,他们也都具有公平性。既他们都有公平锁和非公平锁的实现。实现方式也差不太远,关于公平性的内容可以查看上一篇博客

二、ReentrantReadWriteLock中的读锁与写锁

  ReentrantReadWriteLock 提供一个读写分离的锁,读锁由ReadLock控制,写锁由WriteLock完成。当然读与写是互斥的。如你所知,可读不写,可写不读,即是读写不能同时进行,这就是读写锁

。之所以能做到读写互斥说明他们最终还是用了同一个同步器(Sync),他们依赖于上层(ReentrantReadWriteLock)的同步器,Sync只有一个,所以读锁与写锁不能同时使用

通过查看源码,我们可以得到ReentrantReadWriteLock的以下几个特点:

  1.当读锁被持有,不管是被一人持有,还是多人持有,写都需要阻塞。

  2.当写锁被持有,当然只有一人能持有(独占), 读锁将会被阻塞

  3.读写锁的阻塞方式直接由公平性决定,由FairSync  或  NonFairSync实现

   4.读锁可以有多人同时持有,HoldCounter的作用就是当前线程持有共享锁的数量

ReentrantReadWriteLock  中的 WriteLock写锁的获取和释放过程和 ReentrantLock 几乎相同,都是独占排他锁,都是使用了AQS的acquire/release操作

ReadLock读锁就有区别了,前面我们提到 AQS提供了几个抽象方法让子类去实现,其中就有  tryAcquireShared tryReleaseShared,用于共享锁的获取和释放,所以我们这里重点关注下 读锁的获取和释放过程:

  读取的获取

  1. protected final int tryAcquireShared(int unused) {
  2. Thread current = Thread.currentThread();
  3. //锁的持有线程数
  4. int c = getState();
  5. /*
  6. * 如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
  7. */
  8. if (exclusiveCount(c) != 0 &&
  9. getExclusiveOwnerThread() != current)
  10. return -1;
  11. //读锁线程数
  12. int r = sharedCount(c);
  13. /*
  14. * readerShouldBlock():读锁是否需要等待(公平锁原则)
  15. * r < MAX_COUNT:持有线程小于最大数(65535)
  16. * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
  17. */
  18. if (!readerShouldBlock() && r < MAX_COUNT &&
  19. compareAndSetState(c, c + SHARED_UNIT)) {
  20. /*
  21. * holdCount部分后面讲解
  22. */
  23. if (r == 0) {
  24. firstReader = current;
  25. firstReaderHoldCount = 1;
  26. } else if (firstReader == current) {
  27. firstReaderHoldCount++; //
  28. } else {
  29. HoldCounter rh = cachedHoldCounter;
  30. if (rh == null || rh.tid != current.getId())
  31. cachedHoldCounter = rh = readHolds.get();
  32. else if (rh.count == 0)
  33. readHolds.set(rh);
  34. rh.count++;
  35. }
  36. return 1;
  37. }
  38. return fullTryAcquireShared(current);
  39. }

  读锁的获取过程分析:

    1.如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程(为什么还要满足这个条件,是为了实现 锁降级),那么读取失败

    2.如果读线程请求锁数量达到了 65535(包括重入的部分)(state),那么就抛出异常

    3.如果读线程不用等待(实际上是 是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功,否则执行下一步

    4.上一步失败的原因是 CAS操作修改状态数失败,那么就需要循环不断尝试去修改状态直到成功 或者 锁被写入线程占有

HoldCounter

  前面我们提到,读锁可以有多人同时持有,HoldCounter的作用就是记录当前线程持有共享锁的数量(不是记录所有读线程的共享锁数量,那个由state去记录),下面我们看看 HoldCounter是什么东西,又是如何完成计数的

  首先我们看到只有在获取共享锁(读锁)的时候 + 1,也只有在释放共享锁的时候 - 1 ,会起作用

  强调一下,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一个计数器操作,一次获取共享锁相当于计数器 + 1,释放一个共享锁相当于计数器 - 1.显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有的线程数,包括重入数)错误

  先看读锁获取锁的部分:

  1. if (r == 0) { //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
  2. firstReader = current;
  3. firstReaderHoldCount = 1;
  4. } else if (firstReader == current) { //第一个读锁线程重入
  5. firstReaderHoldCount++;
  6. } else { //非firstReader计数
  7. HoldCounter rh = cachedHoldCounter; //readHoldCounter缓存
  8. //rh == null 或者 rh.tid != current.getId(),需要获取rh
  9. if (rh == null || rh.tid != current.getId())
  10. cachedHoldCounter = rh = readHolds.get();
  11. else if (rh.count == 0)
  12. readHolds.set(rh); //加入到readHolds中
  13. rh.count++; //计数+1
  14. }

  HoldCounter的定义:

  1. static final class HoldCounter {
  2. int count = 0;
  3. final long tid = Thread.currentThread().getId();
  4. }

  在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。

  诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:

  1. static final class ThreadLocalHoldCounter
  2. extends ThreadLocal<HoldCounter> {
  3. public HoldCounter initialValue() {
  4. return new HoldCounter();
  5. }
  6. }

  故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已

四、总结

  关于ReentrantReadWriteLock里面的内容还有很多,有时间还可以去看看源码去品一品,

  在Java1.8之前,它是JDK实现的读写锁(ReadWriteLock)的唯一实现。他由读、写锁两部分组成,写是独占锁,而读是共享锁,且读写互斥

  ReentrantReadWriteLock和ReentrantLock并无关系,但是他们有很多类似的地方,比如都具有可重入性、都有两种获取锁的策略:公平与非公平,与ReentrantLock一样在非公平模式能获得更高的OPS

6.JUC之ReentrantReadWriteLock的更多相关文章

  1. JUC 一 ReentrantReadWriteLock

    java.util.concurrent.locks ReentrantLock是独占锁,一种相对比较保守的锁策略,在这种情况下任何"读/读"."读/写".&q ...

  2. 最强Java并发编程详解:知识点梳理,BAT面试题等

    本文原创更多内容可以参考: Java 全栈知识体系.如需转载请说明原处. 知识体系系统性梳理 Java 并发之基础 A. Java进阶 - Java 并发之基础:首先全局的了解并发的知识体系,同时了解 ...

  3. Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock

    概要 Java的JUC(java.util.concurrent)包中的锁包括"独占锁"和"共享锁".在“Java多线程系列--“JUC锁”02之 互斥锁Ree ...

  4. Java - "JUC" ReentrantReadWriteLock

    Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock ReadWriteLock 和 ReentrantReadWriteLock介绍 ReadWriteLo ...

  5. 【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!

    好长一段时间前,某些场景需要JUC的读写锁,但在某个时刻内读写线程都报超时预警(长时间无响应),看起来像是锁竞争过程中出现死锁(我猜).经过排查项目并没有能造成死锁的可疑之处,因为业务代码并不复杂(仅 ...

  6. 【JUC】JDK1.8源码分析之ReentrantReadWriteLock(七)

    一.前言 在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁 ...

  7. 【JUC】JDK1.8源码分析之ReentrantReadWriteLock

    重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少.然而读服务不存在数据竞争问题,如果一个线程在读 ...

  8. JUC——线程同步锁(ReentrantReadWriteLock读写锁)

    读写锁简介 所谓的读写锁值得是两把锁,在进行数据写入的时候有一个把“写锁”,而在进行数据读取的时候有一把“读锁”. 写锁会实现线程安全同步处理操作,而读锁可以被多个对象读取获取. 读写锁:ReadWr ...

  9. 【JUC源码解析】ReentrantReadWriteLock

    简介 ReentrantReadWriteLock, 可重入读写锁,包括公平锁和非公平锁,相比较公平锁而言,非公平锁有更好的吞吐量,但可能会出现队列里的线程无限期地推迟一个或多个读线程或写线程的情况, ...

随机推荐

  1. IFC构件位置信息—ObjectPlacement

    在IFC标准中,采用相对坐标系对构件定位.如柱(IfcColumn)的定位信息(局部坐标系及参考坐标系)由ObjectPlacement描述.ObjectPlacement由两部分组成: (1)Pla ...

  2. dtypes.py", line 499 _np_qint8 = np.dtype([("qint8", np.int8, (1,)])

    Traceback (most recent call last): File "<stdin>", line 1, in <module> File &q ...

  3. hugepage设置相关总结

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/shaoyunzhe/article/de ...

  4. 数据结构与抽象 Java语言描述 第4版 pdf (内含标签)

    数据结构与抽象 Java语言描述 第4版 目录 前言引言组织数据序言设计类P.1封装P.2说明方法P.2.1注释P.2.2前置条件和后置条件P.2.3断言P.3Java接口P.3.1写一个接口P.3. ...

  5. FreeMarker的应用场景

      FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页.电子邮件.配置文件.源代码等)的通用工具. 它不是面向最终用户的,而是一个Java类库,是一 ...

  6. delphi self.Update 什么作用

    更新指定窗口的客户区.如果窗口更新的区域不为空,UpdateWindow函数就发送一个WM_PAINT消息来更新指定窗口的客户区.函数绕过应用程序的消息队列,直接发送WM_PAINT消息给指定窗口的窗 ...

  7. TOmCAT HTTPS 单向验证 忽略证书

    https://www.cnblogs.com/haha12/p/4381663.html

  8. SQL命令如何分发到集群的各节点

    有些数据库集群的规模是很大的,有上百个节点,那么维护SQL命令如何快速分发给各个节点,例如:要加个字段,逐个节点操作那是十分低效,枯燥的. TreeSoft增加了[SQL分发]功能,简单配置,可以快速 ...

  9. 智能指针.Qt测试

    1.Qt598x64vs2017(或 Qt598x86vs2015[配置使用vs2017]).Win10x64 2.测试代码: 2.1.MainWindow.h class MainWindow : ...

  10. art-template 弹出编辑

    <!-- 模板 --> <script id="render-tpl" type="text/html"> <div class= ...