ReentrantLock实现了标准的互斥重入锁,任一时刻只有一个线程能获得锁。考虑这样一个场景:大部分时间都是读操作,写操作很少发生;我们知道,读操作是不会修改共享数据的,如果实现互斥锁,那么即使都是读操作,也只有一个线程能获得锁,其他的读都得阻塞。这样显然不利于提供系统的并发量。在JDK1.5中,Doug Lea又给我们带来了读写锁ReentrantReadWriteLock,在读-写锁的实现加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。

ReentrantReadWriteLock的结构

ReentrantReadWriteLock(简称RRWL)的实现也是通过AQS来实现的,它的内部也与一个Sync类,继承自AQS,读写锁都是依赖它来实现。在RRWL内部有两个类来分别实现读锁和写锁,ReadLock和WriteLock。RRWL有两个方法分别用于返回读锁和写锁:

ReentrantReadWriteLock.ReadLock readLock()

返回读锁
   
ReentrantReadWriteLock.WriteLock writeLock()

返回写锁

同样的,RRWL使用FairSync实现公平策略,NonFairSync实现非公平策略。RRWL包含的类:

Sync类

在读写锁中最重要的就是Sync类,它继承自AQS,还记得吗,AQS使用一个int型来保存状态,状态在这里就代表锁,它提供了获取和修改状态的方法。可是,这里要实现读锁和写锁,只有一个状态怎么办?Doug Lea是这么做的,它把状态的高16位用作读锁,低16位用作写锁,所以无论是读锁还是写锁最多只能被持有65535次。所以在判断读锁和写锁的时候,需要进行位运算:

(1)由于读写锁共享状态,所以状态不为0,只能说明是有锁,可能是读锁,也可能是写锁;

(2)读锁是高16为表示的,所以读锁加1,就是状态的高16位加1,低16位不变,所以要加的不是1,而是2^16,减一同样是这样。

(3)写锁用低16位表示,要获得写锁的次数,要用状态&2^16-1,结果的高16位全为0,低16位就是写锁被持有的次数。

在Sync中还有几个属性,会在后面的代码中用到。

[java] view plaincopy
  1. /** 实现ReentrantReadWriteLock的同步器,分别用子类来实现公平和非公平策略 */
  2. abstract static class Sync extends AbstractQueuedSynchronizer {
  3. private static final long serialVersionUID = 6317671515068378041L;
  4. //最多支持65535个写锁和65535个读锁;低16位表示写锁计数,高16位表示持有读锁的线程数
  5. static final int SHARED_SHIFT   = 16;
  6. //由于读锁用高位部分,读锁个数加1,其实是状态值加 2^16
  7. static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
  8. static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
  9. /**写锁的掩码,用于状态的低16位有效值 */
  10. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  11. /** 读锁计数,当前持有读锁的线程数,c的高16位 */
  12. static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  13. /** 写锁的计数,也就是它的重入次数,c的低16位*/
  14. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  15. /**
  16. * 每个线程持有读锁的计数
  17. */
  18. static final class HoldCounter {
  19. int count = 0;
  20. //使用id而不是引用是为了避免保留垃圾。注意这是个常量。
  21. final long tid = Thread.currentThread().getId();
  22. }
  23. /**
  24. * 采用继承是为了重写 initialValue 方法,这样就不用进行这样的处理:
  25. * 如果ThreadLocal没有当前线程的计数,则new一个,再放进ThreadLocal里。
  26. * 可以直接调用 get。
  27. * */
  28. static final class ThreadLocalHoldCounter
  29. extends ThreadLocal<HoldCounter> {
  30. public HoldCounter initialValue() {
  31. return new HoldCounter();
  32. }
  33. }
  34. /**
  35. * 当前线程持有的可重入读锁的数量,仅在构造方法和readObject(反序列化)
  36. * 时被初始化,当持有锁的数量为0时,移除此对象。
  37. */
  38. private transient ThreadLocalHoldCounter readHolds;
  39. /**
  40. * 最近一个成功获取读锁的线程的计数。这省却了ThreadLocal查找,
  41. * 通常情况下,下一个释放线程是最后一个获取线程。这不是 volatile 的,
  42. * 因为它仅用于试探的,线程进行缓存也是可以的
  43. * (因为判断是否是当前线程是通过线程id来比较的)。
  44. */
  45. private transient HoldCounter cachedHoldCounter;
  46. /**firstReader是第一个获得读锁的线程;
  47. * firstReaderHoldCount是firstReader的重入计数;
  48. * 更准确的说,firstReader是最后一个把共享计数从0改为1,并且还没有释放锁。
  49. * 如果没有这样的线程,firstReader为null;
  50. * firstReader不会导致垃圾堆积,因为在tryReleaseShared中将它置空了,除非
  51. * 线程异常终止,没有释放读锁。
  52. *
  53. * 跟踪无竞争的读锁计数时,代价很低
  54. */
  55. private transient Thread firstReader = null;
  56. private transient int firstReaderHoldCount;
  57. Sync() {
  58. readHolds = new ThreadLocalHoldCounter();
  59. setState(getState()); // ensures visibility of readHolds
  60. }

它的两个子类,FairSync和NonFairSync比较简单,它们就是决定在某些情况下读锁或者写锁是否需要阻塞,通过两个方法的返回值决定:

final boolean writerShouldBlock();//写锁是否需要阻塞

final boolean readerShouldBlock();//读锁是否需要阻塞

获取读锁

读锁是共享锁,同一时刻可以被多个线程获得,下面是获得读锁的代码:

[java] view plaincopy
  1. /**
  2. * 获取读锁,如果写锁不是由其他线程持有,则获取并立即返回;
  3. * 如果写锁被其他线程持有,阻塞,直到读锁被获得。
  4. */
  5. public void lock() {
  6. sync.acquireShared(1);
  7. }
  8. //ASQ的acquireShared
  9. /**
  10. * 以共享模式获取对象,忽略中断。通过至少先调用一次 tryAcquireShared(int)
  11. * 来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquireShared(int)
  12. *  将线程加入队列,线程可能重复被阻塞或不被阻塞。
  13. */
  14. public final void acquireShared(int arg) {
  15. if (tryAcquireShared(arg) < 0)
  16. doAcquireShared(arg);
  17. }
  18. //Sync中的tryAcquireShared
  19. protected final int tryAcquireShared(int unused) {
  20. Thread current = Thread.currentThread();
  21. int c = getState();
  22. //持有写锁的线程可以获得读锁
  23. if (exclusiveCount(c) != 0 &&
  24. getExclusiveOwnerThread() != current)
  25. return -1;//写锁被占用,且不是由当前线程持有,返回-1
  26. //执行到这里表明:写锁可用,或者写锁由当前线程持有
  27. //获得读锁的数量
  28. int r = sharedCount(c);
  29. /** 如果不用阻塞,且没有溢出,则使用CAS修改状态,并且修改成功 */
  30. if (!readerShouldBlock() &&
  31. r < MAX_COUNT &&
  32. compareAndSetState(c, c + SHARED_UNIT)) {//修改高16位的状态,所以要加上2^16
  33. //这是第一个占有读锁的线程,设置firstReader
  34. if (r == 0) {
  35. firstReader = current;
  36. firstReaderHoldCount = 1;
  37. } else if (firstReader == current) {//重入计数加1
  38. firstReaderHoldCount++;
  39. } else {
  40. // 非 firstReader 读锁重入计数更新
  41. //将cachedHoldCounter设置为当前线程
  42. HoldCounter rh = cachedHoldCounter;
  43. if (rh == null || rh.tid != current.getId())
  44. cachedHoldCounter = rh = readHolds.get();
  45. else if (rh.count == 0)
  46. readHolds.set(rh);
  47. rh.count++;
  48. }
  49. return 1;
  50. }
  51. //获取读锁失败,放到循环里重试
  52. return fullTryAcquireShared(current);
  53. }

重点关注Sync中的tryAcquireShared(int),注意,在所有的读写锁中,获取锁和释放锁每次都是一个计数行为,锁其计数都说是1,而在获得读锁的过程中,参数根本就没有意义。上面的代码包含的逻辑:

(1)如果当前写锁被其他线程持有,则获取读锁失败;

(2)写锁空闲,或者写锁被当前线程持有(写锁可降级为读锁),在公平策略下,它可能需要阻塞,那么tryAcquireShared()就可能失败,则需要进入队列等待;如果是非公平策略,会尝试获取锁,使用CAS修改状态,修改成功,则获得读锁,否则也会进入同步队列等待;

(3)进入同步队列后,就是由AQS来完成唤醒。

释放读锁

一般来说,释放锁比获取锁要容易一些,看一下释放读锁的代码:

[java] view plaincopy
  1. protected final boolean tryReleaseShared(int unused) {
  2. //将当前线程的读锁计数器的值减1
  3. Thread current = Thread.currentThread();
  4. /**
  5. * 当前线程是第一个获取到锁的,如果此线程要释放锁了,则firstReader置空
  6. * 否则,将线程持有的锁计数减1
  7. */
  8. if (firstReader == current) {
  9. // assert firstReaderHoldCount > 0;
  10. if (firstReaderHoldCount == 1)
  11. firstReader = null;
  12. else
  13. firstReaderHoldCount--;
  14. } else {
  15. HoldCounter rh = cachedHoldCounter;
  16. //如果cachedHoldCounter为空,或者不等于当前线程
  17. if (rh == null || rh.tid != current.getId())
  18. rh = readHolds.get();
  19. int count = rh.count;
  20. if (count <= 1) {
  21. readHolds.remove();
  22. if (count <= 0)//如果没有持有读锁,释放是非法的
  23. throw unmatchedUnlockException();
  24. }
  25. --rh.count;
  26. }
  27. //有可能其他线程也在释放读锁,所以要确保释放成功
  28. for (;;) {
  29. int c = getState();
  30. int nextc = c - SHARED_UNIT;//高16位-1
  31. if (compareAndSetState(c, nextc))
  32. // 释放读锁对其他读线程没有任何影响,
  33. // 但可以允许等待的写线程继续,如果读锁、写锁都空闲。
  34. return nextc == 0;
  35. }
  36. }

释放读锁很简单,就是把状态的高16位减1,同时把当前线程持有锁的计数减1。在释放的过程中,其他线程可能也在释放读锁,所以修改状态有可能失败,把修改状态放到循环里做,直到成功为止。

写锁的获取

[java] view plaincopy
  1. protected final boolean tryAcquire(int acquires) {
  2. Thread current = Thread.currentThread();
  3. //获取状态,是读写锁共有的
  4. int c = getState();
  5. //写锁被持有的次数,通过与低16位做与操作得到
  6. int w = exclusiveCount(c);
  7. //c!=0,说明存在锁,可能是读锁,也可能是写锁
  8. if (c != 0) {
  9. // c!=0,w==0,说明读锁存在
  10. //w != 0 && current != getExclusiveOwnerThread() 表示其他线程获取了写锁。
  11. if (w == 0 || current != getExclusiveOwnerThread())
  12. return false;
  13. //如果超过了最大限制,则抛出异常
  14. if (w + exclusiveCount(acquires) > MAX_COUNT)
  15. throw new Error("Maximum lock count exceeded");
  16. //执行到这里,说明存在写锁,且由当前线程持有
  17. // 重入计数
  18. setState(c + acquires);
  19. return true;
  20. }
  21. //执行到这里,说明不存在任何锁
  22. //WriterShouldBlock留给子类实现公平策略
  23. //使用CAS修改状态
  24. if (writerShouldBlock() ||
  25. !compareAndSetState(c, c + acquires))
  26. return false;
  27. setExclusiveOwnerThread(current);
  28. return true;
  29. }

其包含的逻辑:

(1)首先获得状态,保存到c中,获得写锁的计数保存到w中;这个时候需要根据c的值来判断是否存在锁

(2)如果c!=0,说明存在锁,如果w==0,说明存在读锁,获取写锁不能成功;如果w!=0,但是写锁是由其他线程持有的,那么当前线程获取写锁也不能成功;在这种情况下(存在写锁),只有写锁是由当前线程持有的,才能获得成功;

(3)如果c==0,说明不存在锁,如果是公平策略,还需要进入同步队列;如果是非公平策略,会尝试获得写锁。

释放写锁

[java] view plaincopy
  1. protected final boolean tryRelease(int releases) {
  2. if (!isHeldExclusively())
  3. throw new IllegalMonitorStateException();
  4. int nextc = getState() - releases;
  5. boolean free = exclusiveCount(nextc) == 0;
  6. //如果锁是可用的
  7. if (free)
  8. setExclusiveOwnerThread(null);
  9. setState(nextc);
  10. return free;
  11. }

释放写锁很简答,就是状态的低16为减1,如果为0,说明写锁可用,返回true,如果不为0,说明当前线程仍然持有写锁,返回false;
上面只是简单的介绍了ReentrantReadWriteLock的实现,过会还会补充。

转载请注明:喻红叶《Java并发-ReentrantReadWriteLock源码分析》

https://blog.csdn.net/yuhongye111/article/details/39055531

Java并发-ReentrantReadWriteLock源码分析相关推荐

  1. 喻红叶《Java并发-ReentrantReadWriteLock源码分析》

    ReentrantLock实现了标准的互斥重入锁,任一时刻只有一个线程能获得锁.考虑这样一个场景:大部分时间都是读操作,写操作很少发生:我们知道,读操作是不会修改共享数据的,如果实现互斥锁,那么即使都 ...

  2. java并发:join源码分析

    join join join是Thread方法,它的作用是A线程中子线程B在运行之后调用了B.join(),A线程会阻塞直至B线程执行结束 join源码(只有继承Thread类才能使用) 基于open ...

  3. 【Java并发编程】16、ReentrantReadWriteLock源码分析

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

  4. 并发编程5:Java 阻塞队列源码分析(下)

    上一篇 并发编程4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueue 和 PriorityBlockingQueue,这篇文 ...

  5. java.util.ServiceLoader源码分析

    java.util.ServiceLoader源码分析 回顾: ServiceLoader类的使用(具体参考博客http://blog.csdn.net/liangyihuai/article/det ...

  6. Java集合类框架源码分析 之 LinkedList源码解析 【4】

    上一篇介绍了ArrayList的源码分析[点击看文章],既然ArrayList都已经做了介绍,那么作为他同胞兄弟的LinkedList,当然必须也配拥有姓名! Talk is cheap,show m ...

  7. java join 源码_java并发:join源码分析

    join join join是Thread方法,它的作用是A线程中子线程B在运行之后调用了B.join(),A线程会阻塞直至B线程执行结束 join源码(只有继承Thread类才能使用) 基于open ...

  8. JUC - ReentrantReadWriteLock 源码分析

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

  9. Java容器 | 基于源码分析List集合体系

    一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...

最新文章

  1. SpringCloud Alibaba微服务实战(六) - 路由网关(Gateway)
  2. 梅花桩上练真功,腾讯公布机器人移动技术探索新突破
  3. Delphi获取显卡和系统各种音频设备的代码实现
  4. echarts控制只显示部分数据的折线图_Python数据可视化之pyecharts入门
  5. Web2.0时代,你得到什么?
  6. jwt重放攻击_【干货分享】基于JWT的Token认证机制及安全问题
  7. 运行报错Error:(29, 41) java: 无法将类 com.imooc.dataobject.ProductCategory中的构造器 ProductCategory应用到给定类型
  8. 根据输入时间判断年龄是否在18~68周岁之间
  9. 【雷达信号处理】---模糊函数与仿真
  10. linux磁盘所有格式化命令,Linux磁盘格式化命令的详细说明
  11. command命令大全(转自http://blog.dhedu.gov.cn/u/72/archives/2009/14290.html)
  12. 计算机软件与硬件的关系及软件的分类
  13. Unity 3D中的射线与碰撞检测 1
  14. android CMWAP CMNET
  15. 毕业工作五年的总结和感悟(中)
  16. 实时系统静态调度和动态调度
  17. Maxon收购Redshift Rendering Technologies
  18. 如何登录锐捷设备(无线篇)
  19. 运行mongoDB时提示:无法启动此程序,因为计算机中丢失api-ms-win-crt-runtime-|1-1-0.dll请重新安装此程序解决问题 解决办法
  20. 当代资本主义的革命动力是什么?

热门文章

  1. 单元测试是什么?怎么写?主要测试什么?
  2. 求单链表的最大值与原地逆转_数据结构基础复习09.ppt
  3. css 绘制三角形箭头
  4. 21天转型容器实战营(一了解容器的基本知识)
  5. 【Markdown】上下标
  6. 使用ivx画布组件打印微信头像的经验总结
  7. z390 黑苹果启动盘_黑苹果硬件选购指南之ITX篇补充说明
  8. (原创)WPF写的台球附源码
  9. 【华为云数据库技术大公开】机房失火后,还能拯救你的数据吗?
  10. CMD 常用指令 超级全