ReentrantReadWriteLock原理
原文链接:https://www.jianshu.com/p/9f98299a17a5
前言
本篇适用于了解ReentrantLock或ReentrantReadWriteLock的使用,但想要进一步了解原理的读者。见于之前的分析都是借鉴大量的JDK源码,这次以流程图的形式代替源码,希望读者能有更好的阅读体验。有兴趣了解源码的读者也可以借鉴本篇的分析成果做源码分析。
所谓** “独占” 即同一时间只能有一个线程持有锁。而 “重入” **是指该线程如果持有锁,可以在同步代码块内再次请求占有锁而不被阻塞,线程重入后将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,表示当前读锁可能被一个或多个线程占有,多于一个占有读锁的线程,允许重入。
读锁竞争
读锁的获取条件要满足:
- ** 当前的写锁未被占有(AQS state变量低16位为0) 或者当前线程是写锁占有的线程**
- ** readerShouldBlock()方法返回false **
- ** 当前读锁占有量小于最大值(2^16 -1) **
- ** 成功通过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类型的节点的唤醒。然后进入读锁同步区域。被唤醒的线程会继续尝试获取读锁,获取读锁成功后就继续上述步骤,这样就保证了队列中几个连续的等待读锁的线程被依次唤醒进入读锁同步区。**
读锁释放
读锁的释放过程即AQS的state高16位同步递减为0的过程,当state的高16位都为0表示读锁释放完毕,如果此时写锁状态为0(即该读锁不是写锁降级来的),唤醒head节点后下一个SIGNAL状态的节点的线程,一般为等待写锁的节点。如果读锁的占有数不为0,表示读锁未完全释放。或者写锁的占有数不为0,表示释放的读锁是写锁降级来的。
写锁分析
写锁的状态表示为AQS的state变量的低16位,当state低16位为0,表示当前写锁没有被占有,反之表示写锁被某个写线程占有(state = 1)或重入(state > 1)。
写锁竞争
写锁获取的条件需要满足:
- ** 读锁未被占用(AQS state高16位为0) ,写锁未被占用(state低16位为0)或者占用写锁的线程是当前线程**
- ** writerShouldBlock()方法返回false,即不阻塞写线程 **
- ** 当前写锁占有量小于最大值(2^16 -1),否则抛出Error("Maximum lock count exceeded") **
- ** 通过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节点,返回中断标记并进入同步代码区。** 与读操作不同的是写操作之间是互斥的,所以获取写锁后不会将下一个申请写操作的节点唤醒。**
写锁释放
写锁的释放过程即AQS的state低16位同步递减为0的过程,当state的高16位都为0表示写锁释放完毕,唤醒head节点后下一个SIGNAL状态的节点的线程。如果该写锁占有线程未释放写锁前还占用了读锁,那么写锁释放后该线程就完全转换成了读锁的持有线程。
小结
- 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
- 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
- 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
- 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
- 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。
ReentrantReadWriteLock原理的更多相关文章
- 【原创】读写锁ReentrantReadWriteLock原理分析(一)
Java里面真正意义的锁并不多,其实真正的实现Lock接口的类就三个,ReentrantLock和ReentrantReadWriteLock的两个内部类(ReentrantReadWriteLock ...
- Java架构系列问题合集-目录
接下来会做一个系列, 分类说明关于Java项目研发和架构工作需要了解的问题 Java语法 Java语法专题1: 类初始化的构造顺序 https://www.cnblogs.com/milton/p/1 ...
- Java多线程专题5: JUC, 锁
合集目录 Java多线程专题5: JUC, 锁 什么是可重入锁.公平锁.非公平锁.独占锁.共享锁 可重入锁 ReentrantLock A ReentrantLock is owned by the ...
- concrrent类下ReentrantReadWriteLock类的原理以及使用
1.ReentrantreadWriteLock 类的介绍 Lock接口下的子类存在 ReentrantLock子类,该子类是一个线程同步处理类:ReentrantLock类的介绍详见XXX: Loc ...
- 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理
(一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...
- ReentrantReadWriteLock实现原理
在java并发包java.util.concurrent中,除了重入锁ReentrantLock外,读写锁ReentrantReadWriteLock也很常用.在实际开发场景中,在使用共享资源时,可能 ...
- 轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理
转载:https://blog.csdn.net/yanyan19880509/article/details/52435135 前言 前面介绍了java中排它锁,共享锁的底层实现机制,本篇再进一步, ...
- 快进来!花几分钟看一下 ReentrantReadWriteLock 的原理!
前言 在看完 ReentrantLock 之后,在高并发场景下 ReentrantLock 已经足够使用,但是因为 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而很多应用场景都 ...
- ReentrantReadWriteLock读写锁简单原理案例证明
ReentrantReadWriteLock存在原因? 我们知道List的实现类ArrayList,LinkedList都是非线程安全的,Vector类通过用synchronized修饰方法保证了Li ...
随机推荐
- 学习vue
一,声明模板的时候需要新建示例 如下代码 <div id="app"> <my></my> </div> Vue.component ...
- AngularJS:参考手册
ylbtech-AngularJS:参考手册 1.返回顶部 1. AngularJS 参考手册 AngularJS 指令 本教程用到的 AngularJS 指令 : 指令 描述 ng-app 定义应用 ...
- Log4j配置记录(特定java包/类的日志级别控制)
最近使用log4j,关于日志级别的如何配置生效百思不得其解,花了些时间,误打误撞终于整了,记录一下,备忘. 注意: 1.图中的2(log4j.logger.com.taobao)限制级别最高,它直接指 ...
- linux下不用空格执行带参数的5种姿势
在搞安全的时候经常会遇到代码/命令执行,不能用空格的情况,总结了几种的绕过方法. 1.!! [root@iZ28wg1kditZ tmp]# pwd /tmp [root@iZ28wg1kditZ t ...
- 2011-12-14 调用cmd并获得输入输出+网络访问
System.Diagnostics.Process pro = new System.Diagnostics.Process(); pro.StartInfo.FileName = "cm ...
- 第五章 深入class文件结构(待续)
JVM指令集简介 class文件头的表示形式 常量池 类信息 Fields和Methods定义 类属性描述 Javap生成的class文件结构
- javascript——正则表达式(RegExp、String)(未完工)
在 javascript 中,正则表达式由两部分组成:正则表达式的匹配模式文本:匹配模式文本的修饰符: 修饰符: 修饰符 说明 i 忽略大小写 g 执行全局匹配 m 执行多行匹配 匹配模式文本包括以下 ...
- mybaits中date类型显示时分秒(orcle数据库)
<insert id="insert" parameterType="daSysLoginLog"> insert into DA_SYS_LOGI ...
- iter创建一个可以被迭代的对象
#!/usr/bin/env python obj = iter([11,22,33,44]) #iter 创建一个可以被迭代的对象 print(obj) r1 = next(obj) print(r ...
- 详解CSS display:inline-block的应用(转)
详解CSS display:inline-block的应用 阅读目录 基础知识 inline-block的问题 inline-block的应用 总结 本文详细描述了display:inline-b ...