前言

本篇适用于了解ReentrantLock或ReentrantReadWriteLock的使用,但想要进一步了解原理的读者。见于之前的分析都是借鉴大量的JDK源码,这次以流程图的形式代替源码,希望读者能有更好的阅读体验。有兴趣了解源码的读者也可以借鉴本篇的分析成果做源码分析。

《从源码分析ReentrantLock原理》这一篇文章中分析了以非阻塞同步算法为基础实现的可重入独占锁ReentrantLock。所谓** “独占” 即同一时间只能有一个线程持有锁。而 “重入” **是指该线程如果持有锁,可以在同步代码块内再次请求占有锁而不被阻塞,线程重入后将AQS内部状态state同步加1继续同步区的操作。但是要注意该线程要想移交锁的控制权必须完全释放重入锁,即将AQS的state同步更新到0为止。

ReentrantReadWriteLock出现的目的就是针对ReentrantLock独占带来的性能问题,使用ReentrantLock无论是“写/写”线程、“读/读”线程、“读/写”线程之间的工作都是互斥,同时只有一个线程能进入同步区域。然而大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock,它的特性是:** 一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。**从而提高读操作的吞吐量。

初识ReentrantReadWriteLock

ReentrantReadWriteLock并没有继承ReentrantLock,也并没有实现Lock接口,而是实现了ReadWriteLock接口,该接口提供readLock()方法获取读锁,writeLock()获取写锁。

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable { private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock; public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
} public interface ReadWriteLock { Lock readLock(); Lock writeLock();
}

默认构造方法为** 非公平模式 ,开发者也可以通过指定fair为true设置为 公平模式 **。

    public ReentrantReadWriteLock() {
this(false);
} public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
} public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}

而公平模式和非公平模式分别由内部类FairSync和NonfairSync实现,这两个类继承自另一个内部类Sync,该Sync继承自AbstractQueuedSynchronizer(以后简称** AQS **),这里基本同ReentrantLock的内部实现一致。

abstract static class Sync extends AbstractQueuedSynchronizer {
} static final class FairSync extends Sync {
} static final class NonfairSync extends Sync {
}

在ReentrantLock的分析中得知,其独占性和重入性都是通过CAS操作维护AQS内部的state变量实现的。ReentrantReadWriteLock将这个int型state变量分为高16位和低16位,高16位表示当前读锁的占有量低16位表示写锁的占有量,详见ReentrantReadWriteLock的内部类Sync :

abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
...
}

读锁分析

读锁,锁定的是AQS的state变量的高16位,当state的高16位等于0,表示当前读锁未被占有;当state的高16位大于0,表示当前读锁可能被一个或多个线程占有,多于一个占有读锁的线程,允许重入。

读锁竞争

 
读锁竞争过程.png

读锁的获取条件要满足:

  1. ** 当前的写锁未被占有(AQS state变量低16位为0) 或者当前线程是写锁占有的线程**
  2. ** readerShouldBlock()方法返回false **
  3. ** 当前读锁占有量小于最大值(2^16 -1) **
  4. ** 成功通过CAS操作将读锁占有量+1(AQS的state高16位同步加1) **

条件1使得读锁与写锁互斥,除非当前申请读操作的线程是占有写锁的线程,即实现了写锁降级为读锁。

条件2在非公平模式下执行的是NonfairSync类的readerShouldBlock()方法:

    final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
} final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}

如果AQS的锁等待队列head节点后的节点非共享节点(等待读锁的节点),将返回true。

条件2在公平模式下执行的是FairSync类的readerShouldBlock方法:

    final boolean readerShouldBlock() {
return hasQueuedPredecessors();
} public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

只要AQS锁等待队列的头尾不为空,并且存在head后的节点并且节点的线程非当前线程,返回true。

条件3保证读锁的占有数不超过最大上限,条件4保证多线程竞争读锁时的安全性。

不满足条件申请读锁的线程会被封装为SHARED类型的线程节点插入到AQS锁等待队列的末尾,在插入队列尾后还有一次机会尝试获取读锁。如果还是失败的,下面判断如果队列前一节点是SIGNAL状态就将线程挂起。当线程唤醒后会再次尝试获取读锁,不满足条件会再次挂起,以此循环。

** 如果在线程挂起前获取读锁,下面会将当前节点设置为head节点,并将head后的SHARED类型的节点的唤醒。然后进入读锁同步区域。被唤醒的线程会继续尝试获取读锁,获取读锁成功后就继续上述步骤,这样就保证了队列中几个连续的等待读锁的线程被依次唤醒进入读锁同步区。**

读锁释放

 
读锁的释放.png

读锁的释放过程即AQS的state高16位同步递减为0的过程,当state的高16位都为0表示读锁释放完毕,如果此时写锁状态为0(即该读锁不是写锁降级来的),唤醒head节点后下一个SIGNAL状态的节点的线程,一般为等待写锁的节点。如果读锁的占有数不为0,表示读锁未完全释放。或者写锁的占有数不为0,表示释放的读锁是写锁降级来的。

写锁分析

写锁的状态表示为AQS的state变量的低16位,当state低16位为0,表示当前写锁没有被占有,反之表示写锁被某个写线程占有(state = 1)或重入(state > 1)。

写锁竞争

 
写锁竞争过程.png

写锁获取的条件需要满足:

  1. ** 读锁未被占用(AQS state高16位为0) ,写锁未被占用(state低16位为0)或者占用写锁的线程是当前线程**
  2. ** writerShouldBlock()方法返回false,即不阻塞写线程 **
  3. ** 当前写锁占有量小于最大值(2^16 -1),否则抛出Error("Maximum lock count exceeded") **
  4. ** 通过CAS竞争将写锁状态+1(将state低16位同步+1) **

条件1使得写锁与读锁互斥,ReentrantReadWriteLock并没有读锁升级的功能。

条件2的writerShouldBlock()方法在非公平模式下实现为:

        final boolean writerShouldBlock() {
return false; // writers can always barge
}

即非公平模式下允许满足条件的写操作直接插队。

条件2的writerShouldBlock()方法在公平模式下实现为:

        final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}

公平模式下同读锁一样,如果AQS的锁等待队列不为空,写操作无法插队。

条件3保证写锁占有线程的重入次数不会溢出上限,条件4保证多个写操作的线程竞争写锁的安全性。

不满足获取写锁条件的线程会封装为EXECLUSIVE型的NODE插入到AQS的锁等待队列尾部,通过acquireQueued方法进入循环,该循环内再次尝试获取写锁(因为经过上述操作,另一个锁占有线程可能释放了锁),否则通过shouldParkAfterFailedAcquire方法将前一节点设置为SIGNAL状态后将自身线程挂起。当线程被唤醒后会再次尝试获取写锁,失败则继续挂起,以此循环。或成功占有写锁则将当前Node设置为head节点,返回中断标记并进入同步代码区。** 与读操作不同的是写操作之间是互斥的,所以获取写锁后不会将下一个申请写操作的节点唤醒。**

写锁释放

 
写锁的释放.png

写锁的释放过程即AQS的state低16位同步递减为0的过程,当state的高16位都为0表示写锁释放完毕,唤醒head节点后下一个SIGNAL状态的节点的线程。如果该写锁占有线程未释放写锁前还占用了读锁,那么写锁释放后该线程就完全转换成了读锁的持有线程。

小结

  • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
  • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
  • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
  • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
  • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

作者:Mars_M
链接:https://www.jianshu.com/p/9f98299a17a5
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

可重入读写锁ReentrantReadWriteLock基本原理分析的更多相关文章

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

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

  2. 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁

    上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...

  3. Java基础-Java中的并法库之重入读写锁(ReentrantReadWriteLock)

    Java基础-Java中的并法库之重入读写锁(ReentrantReadWriteLock) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 在学习Java的之前,你可能已经听说过读 ...

  4. 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁

    这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,而且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了怎样模拟一个读写锁. 可重 ...

  5. 【java并发编程】ReentrantLock 可重入读写锁

    目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 欢迎关注我的博客,更多精品知识合集 一.ReentrantLock可重入锁 可 ...

  6. 018-并发编程-java.util.concurrent.locks之-ReentrantReadWriteLock可重入读写锁

    一.概述 ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程.写线程和写线程同时访问.相对 ...

  7. 【原创】读写锁ReentrantReadWriteLock原理分析(一)

    Java里面真正意义的锁并不多,其实真正的实现Lock接口的类就三个,ReentrantLock和ReentrantReadWriteLock的两个内部类(ReentrantReadWriteLock ...

  8. 三、curator recipes之共享的可重入读写锁

    简介 curator实现了跨JVM的可重入读写互斥锁.它使用zookeeper去进行加锁,所以指定相同路径的处理线程将会基于“公平锁”的机制去竞争锁资源. 读写锁包含了读锁.写锁两个,它们的互斥关系如 ...

  9. 并发编程-concurrent指南-ReadWriteLock-ReentrantReadWriteLock(可重入读写锁)

    几个线程都申请读锁,都能获取: import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantRea ...

随机推荐

  1. 006 jquery过滤选择器-----------(可见性过滤选择器)

    1.介绍 2.程序 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> < ...

  2. Git 版本控制管理(一)

    Git 是一个分布式版本控制工具,它的作者 Linus Torvalds 是这样给我们介绍 Git  —— The stupid content tracker(傻瓜式的内容跟踪器) 关于 Git 的 ...

  3. es6 Promise 事件机制分析

    最近在学习es6的Promise,其中涉及到了Promsie的事件执行机制,因此总结了关于Promise的执行机制,若有错误,欢迎纠错和讨论. 在阮一峰老师的书中<es6 标准入门>对Pr ...

  4. Vue.js组件遇到的那些坑

    对于绝大多数开发人员来讲,组件是使用Vue.js不能逃避的话题,当然实际开发也会有很多需要注意的地方,一下均为实际操作中遇到的坑,接下来逐一为大家分享: 1.定义并注册组件必须要在声明Vue实例之前, ...

  5. Ajax json交互和SpringMVC中@RequestBody

    Ajax json交互和SpringMVC中@RequestBody 标签: 背景 自己提供出去得接口中参数设置为@RequestBody VipPromotionLog vipPromotionLo ...

  6. 使用ApiPost模拟发送get、post、delete、put等http请求

    现在的模拟发送请求插件很多比如老外的postman等,但亲测咱们国内的 ApiPost 更好用一些,因为它不仅可以模拟发送get.post.delete.put请求,还可以导出文档,支持团队协作也是它 ...

  7. html (第四本书第九章参考)

    上机1 <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8 ...

  8. 【HDU5909】Tree Cutting(FWT)

    [HDU5909]Tree Cutting(FWT) 题面 vjudge 题目大意: 给你一棵\(n\)个节点的树,每个节点都有一个小于\(m\)的权值 定义一棵子树的权值为所有节点的异或和,问权值为 ...

  9. 11、Redis的持久化(RDB、AOF)

    写在前面的话:读书破万卷,编码如有神 --------------------------------------------------------------------------------- ...

  10. Jenkins部署在Tomcat容器提示:反向代理设置有误

    这个错误我研究了一下,应该是部署在Tomcat时特有的错误,并且我把启动的目录设置成ROOT同样也还在,网上说在系统设置里面的Jenkins URL设置后可以解决,但我测试了同样不行. 如果使用jav ...