ReentrantReadWriteLock包含两把锁,一是读锁ReadLock, 此乃共享锁, 一是写锁WriteLock, 此乃排它锁. 这两把锁都是基于AQS来实现的.

下面通过源码来看看ReentrantReadWriteLock是如何做到读读共享,读写互斥的.

1. 测试代码 

  1. import java.util.concurrent.CyclicBarrier;
  2. import java.util.concurrent.locks.ReentrantReadWriteLock;
  3.  
  4. public class ShareLockTest {
  5. public static void main(String[] args) {
  6. ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  7. ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
  8. ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
  9. CyclicBarrier cyclicBarrier = new CyclicBarrier(50);
  10. for (int i = 1; i <= 50; i++) {
  11. int finalI = i;
  12. new Thread(() -> {
  13. try {
  14. cyclicBarrier.await();
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. if (finalI % 2 == 0) {
  19. System.out.println(Thread.currentThread().getName() + "开始抢写锁");
  20. writeLock.lock();
  21. } else {
  22. System.out.println(Thread.currentThread().getName() + "开始抢读锁");
  23. readLock.lock();
  24. }
  25. try {
  26. System.out.println(Thread.currentThread().getName() + "抢读锁成功");
  27. Thread.currentThread().sleep(1000);
  28. System.out.println(Thread.currentThread().getName() + "释放读锁");
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. } finally {
  32. if (finalI % 2 == 0) {
  33. writeLock.unlock();
  34. } else {
  35. readLock.unlock();
  36. }
  37. }
  38. }, "线程" + i).start();
  39.  
  40. }
  41. System.out.println("main over");
  42. }
  43. }

2. 获取读锁资源

读锁资源的获取通过下面这段代码实现

  1. protected final int tryAcquireShared(int unused) {
  2. // 1. 如果读锁被其它线程持有,失败
  3. // 当前抢锁的线程
  4. Thread current = Thread.currentThread();
  5. // AQS四大属性中的state值
  6. int c = getState();
  7. // 如果持有写锁的线程数量不等于0 且 当前线程不是AQS中的保存的写锁线程 (忽略重入情况)
  8. if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) // 简单讲就是当前线程不是持有写锁的线程就返回-1
  9. return -1; // 获取读锁失败
  10. // 拥有读锁的线程数量
  11. int r = sharedCount(c);
  12.  
  13. if (!readerShouldBlock() // 不需要排队
  14. && r < MAX_COUNT // 拥有读锁的线程数量 小于65535
  15. && compareAndSetState(c, c + SHARED_UNIT)) { // 通过cas将AQS中state值由c修改成c+65536
  16. if (r == 0) {// 如果还没有线程持有读锁
  17. firstReader = current; // 将当前线程赋值给firstReader这个变量,其实就是标识一下
  18. firstReaderHoldCount = 1; // 读锁持有量记为1,以便于这个线程再次获取读锁时进行累加
  19. } else if (firstReader == current) { //如果当前线程等于firstReader,将firstReaderHoldCount加1
  20. firstReaderHoldCount++;
  21. } else { // 如果是其它的线程来获取读锁
  22. // 与上面原理一样,也是一个计数器,来计录每个线程获取读锁的次数(底层使用了一个ThreadLocal)
  23. HoldCounter rh = cachedHoldCounter;
  24. if (rh == null || rh.tid != getThreadId(current))
  25. cachedHoldCounter = rh = readHolds.get();
  26. else if (rh.count == 0)
  27. readHolds.set(rh);
  28. rh.count++;
  29. }
  30. return 1; // 获取读锁成功
  31. }
  32. // 没看懂, 似乎是为了抓捕漏网之鱼
  33. return fullTryAcquireShared(current);
  34. }

以上代码不难, 就是通过 tryAcquireShared获取读锁资源 ,如果获取读锁失败, 就会执行 doAcquireShared 方法. 这个方法有两个功能, 首次是将当前线程封装成一个Node节点(注意该Node是SHARED模式),然后通过addWaiter方法将其添加到CLH链表的尾部. 再次就是将其park.

3. 获取写锁资源

下面这段代码就是尝试获取写锁的过程

  1. protected final boolean tryAcquire(int acquires) {
  2. // 当前线程
  3. Thread current = Thread.currentThread();
  4. // AQS四大属性的state, 只有在即无读锁也无写锁的情况,才等于0
  5. int c = getState();
  6. // 写锁的数量
  7. int w = exclusiveCount(c);
  8.  
  9. if (c != 0) { // 表示已经有线程持有锁(可能是读锁,也可能是写锁)
  10. // (Note: if c != 0 and w == 0 then shared count != 0)
  11. if (w == 0 || current != getExclusiveOwnerThread()) // 无线程持有写锁,或者是持有写锁的线程不是当前线程,返回false
  12. return false;
  13. if (w + exclusiveCount(acquires) > MAX_COUNT)
  14. throw new Error("Maximum lock count exceeded");
  15. // 重入, 持有写锁的线程再次获取锁,对state值进行更新
  16. setState(c + acquires);
  17. return true;
  18. }
  19. if (writerShouldBlock() ||!compareAndSetState(c, c + acquires)) // NonfairSync 默认就是false
  20. return false;
  21. // 当前线程获取到锁
  22. setExclusiveOwnerThread(current);
  23. return true;
  24. }

就是通过上面这段代码来进行写锁获取,可以看到当前线程能否获取到写锁资源, 最终还是通过AQS中exclusiveOwnerThread与当前线程进行比较.

(1) c = 0 , w= 0 时 , 说明还没有线程持有锁资源(读锁和写锁), 这时当前线程获取写锁肯定成功(应该只有第一次获取写锁才会走到下面的逻辑)

  1.   compareAndSetState(c, c + acquires) //将AQS中state设置为1
  2. setExclusiveOwnerThread(current); //将AQS中exclusiveOwnerThread设置为当前线程

(2) c != 0 , w =0时, 先判断写锁数量是否等于0,.如果等于0再判断 exclusiveOwnerThread 是否是当前线程,如果不是,返回false,获取写锁资源失败. 如果是, 表示当前线程再次获取写锁资源了(重入锁的情况), 这时会对AQS对象的state属性值加1, 同时返回true, 获取写锁成功.

总之,  tryAcquire()方法就是尝试获取写锁资源, 如果获取成功,一切好说. 如果获取失败, 就会通过 addWaiter方法将当前线程封装成一个Node节点(注意该节点是EXCLUSIVE模式),放到CLH链表中,然后再通过acquireQueued方法将当前线程进行park.(具体过程可参考AQS源码分析笔记)

4. 读锁释放, 写锁唤醒

咱们通过debug来模拟这样一种情况 . 1号线程和11号线程持有读锁, 10号线程,20号线程获取写锁没成功, 被挂起了.

现在1号线程释放读锁资源, 看看会发生什么情况....

  1. protected final boolean tryReleaseShared(int unused) {
  2. Thread current = Thread.currentThread();
  3. if (firstReader == current) {
  4. // assert firstReaderHoldCount > 0;
  5. if (firstReaderHoldCount == 1)
  6. firstReader = null;
  7. else
  8. firstReaderHoldCount--;
  9. } else {
  10. HoldCounter rh = cachedHoldCounter;
  11. if (rh == null || rh.tid != getThreadId(current))
  12. rh = readHolds.get();
  13. int count = rh.count;
  14. if (count <= 1) {
  15. readHolds.remove();
  16. if (count <= 0)
  17. throw unmatchedUnlockException();
  18. }
  19. --rh.count;
  20. }
  21. for (;;) {
  22. int c = getState();
  23. int nextc = c - SHARED_UNIT;
  24. if (compareAndSetState(c, nextc))
  25.  
  26. return nextc == 0;
  27. }
  28. }
  1.  
  2. tryReleaseShared方法很简单,唯一需要注意的就是标红部分,只有当最终return的结果是true时,才会进入到 doReleaseShared()方法中, 看下源码
  1. private void doReleaseShared() {
  2. for (;;) {
  3. Node h = head;
  4. if (h != null && h != tail) {
  5. int ws = h.waitStatus;
  6. if (ws == Node.SIGNAL) {
  7. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  8. continue; // loop to recheck cases
  9. unparkSuccessor(h); // 唤醒线程
  10. }
  11. else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  12. continue; // loop on failed CAS
  13. }
  14. if (h == head) // loop if head changed
  15. break;
  16. }
  17. }

通过debug, 不难发现, 读锁资源被1号线程和11号线程持有,我先释放1号线程的读锁资源,结果读锁资源并没有释放成功, 我再去11号线程的读锁资源, 结果释放成功. 然后进入到doReleaseShared()方法中,这个方法主要就是去唤醒CLH链表中线程.

5. 写锁释放,唤醒写锁

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

这段代码在讲AQS中也提到过, 如果锁资源释放成功,会通过unparkSuccessor方法唤醒CLH链表中下一个节点的线程, 这时不再多说了.

6. 写锁释放,唤醒读锁

这种情况有点特别, 先是通过释放写锁的线程去唤醒CLH链表中head节点next节点指向的读锁线程, 然后再通过这个读锁线程递归唤醒所有读锁线程

  1. private void unparkSuccessor(Node node) {
  2.  
  3. int ws = node.waitStatus;
  4. if (ws < 0)
  5. compareAndSetWaitStatus(node, ws, 0);
  6.  
  7. Node s = node.next;
  8. if (s == null || s.waitStatus > 0) {
  9. s = null;
  10. for (Node t = tail; t != null && t != node; t = t.prev)
  11. if (t.waitStatus <= 0)
  12. s = t;
  13. }
  14. if (s != null)
  15. LockSupport.unpark(s.thread); // 唤醒CLH链表中第一个读锁线程
  16. }
  1. private void doAcquireShared(int arg) {
  2. final Node node = addWaiter(Node.SHARED);
  3. boolean failed = true;
  4. try {
  5. boolean interrupted = false;
  6. for (;;) {
  7. final Node p = node.predecessor();
  8. if (p == head) {
  9. int r = tryAcquireShared(arg);
  10. if (r >= 0) {
  11. setHeadAndPropagate(node, r);
  12. p.next = null; // help GC
  13. if (interrupted)
  14. selfInterrupt();
  15. failed = false;
  16. return;
  17. }
  18. }
  19. if (shouldParkAfterFailedAcquire(p, node) &&
  20. parkAndCheckInterrupt())
  21. interrupted = true;
  22. }
  23. } finally {
  24. if (failed)
  25. cancelAcquire(node);
  26. }
  27. }

注意标红部分,读锁线程被唤醒之后,for循环就活了,然后就会调用  setHeadAndPropagate(node, r)--->  doReleaseShared() ---->  unparkSuccessor(h),

如果最后这个unparkSuccessor(h)方法中,h节点的下一个节点是读锁线程,那么又会触发一次  setHeadAndPropagate(node, r)--->  doReleaseShared() ---->  unparkSuccessor(h)调用链,直到将所有读锁线程都唤醒.

7. 总结

(1) ReentrantReadWriteLock是在AQS的基础上实现读,写锁分离的过程.

(2) 将state这个属性值 拆分为高低位,来实现读,写锁控制 (有点懵, 位运算,与运算可读性差...)

(3) 读,写锁线程的Node节点仍然是放在CLH链表中的..

(4) 读锁线程唤醒可一次性唤醒多个, 写锁线程一次只能唤醒 一个

ReentrantReadWriteLock源码分析笔记的更多相关文章

  1. zeromq源码分析笔记之线程间收发命令(2)

    在zeromq源码分析笔记之架构说到了zmq的整体架构,可以看到线程间通信包括两类,一类是用于收发命令,告知对象该调用什么方法去做什么事情,命令的结构由command_t结构体确定:另一类是socke ...

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

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

  3. ReentrantReadWriteLock 源码分析

    ReentrantReadWriteLock  源码分析: 1:数据结构: 成员变量: private final ReentrantReadWriteLock.ReadLock readerLock ...

  4. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  5. ArrayList源码分析笔记

    ArrayList源码分析笔记 先贴出ArrayList一些属性 public class ArrayList<E> extends AbstractList<E> imple ...

  6. ReentrantReadWriteLock源码分析(一)

    此处源码分析,主要是基于读锁,非公平机制,JDK1.8. 问题: 1.ReentrantReadWriteLock是如何创建读锁与写锁? 2.读锁与写锁的区别是什么? 3.锁的重入次数与获取锁的线程数 ...

  7. Java显式锁学习总结之五:ReentrantReadWriteLock源码分析

    概述 我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放.共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和 ...

  8. 线程池之ThreadPoolExecutor线程池源码分析笔记

    1.线程池的作用 一方面当执行大量异步任务时候线程池能够提供较好的性能,在不使用线程池的时候,每当需要执行异步任务时候是直接 new 一线程进行运行,而线程的创建和销毁是需要开销的.使用线程池时候,线 ...

  9. ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)

    前言 上一篇讲解了 AQS 的独占锁部分(参看:ReentrantLock 源码分析以及 AQS (一)),这一篇将介绍 AQS 的共享锁,以及基于共享锁实现读写锁分离的 ReentrantReadW ...

随机推荐

  1. 【HANA系列】SAP HANA XS Administration Tool登录参数设置

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[HANA系列]SAP HANA XS Admi ...

  2. 【ABAP系列】SAP BOM反查

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP BOM反查   前言部分 ...

  3. redis缓存与数据一致性

    目录 缓存 缓存穿透 缓存雪崩(缓存失效) 缓存击穿(热点key) 缓存并发竞争(并发set) 数据一致性 缓存(双写)一致性 Redis集群(Redis-cluster)一致性原理 哨兵(Senti ...

  4. ARM汇编指令特点

    根据朱有鹏老师课程笔记整理而来: (汇编)指令是CPU机器指令的助记符,经过编译后会得到一串1 0组成的机器码,由CPU读取执行. (汇编)伪指令本质上不是指令(只是和指令一起写在代码中),它是编译器 ...

  5. yum源迁移(思路具体操作之后加)

    准备工作,有一台能联网的机器装有liunx系统 首先在联网机器下载yum系列包(yum命令如果不存在的话只能通过安装包的形式进行安装这里不考虑yum命令不存在情况) 修改配置文件使得yum命令只下载不 ...

  6. [总集] LOJ 分块1 – 9

    目录 分块9题 出题人hzw的解析 数列分块入门 1 修改:区间加 查询:单点值查询 代码 数列分块入门 2 修改:区间加 查询:区间排名 代码 数列分块入门 6 修改:单点插入 查询:单点值 代码 ...

  7. nodejs版本控制:nvm use命令失效

    Downloading npm version ... Download failed. Rolling Back. Rollback failed. remove C:\Users\Administ ...

  8. sql server优化方向?

    系列转自KK:https://www.cnblogs.com/double-K/ Expert 诊断优化系列------------------你的CPU高么? Expert 诊断优化系列------ ...

  9. 初相识|performance_schema全方位介绍

    初相识|performance_schema全方位介绍 |导 语 很久之前,当我还在尝试着系统地学习performance_schema的时候,通过在网上各种搜索资料进行学习,但很遗憾,学习的效果并不 ...

  10. mysql文本后面带换行符导致查询不到

    UPDATE tablename SET  FIELD = REPLACE(REPLACE(FIELD, CHAR(10), ''), CHAR(13), ''); CHAR(10):  换行符 CH ...