前言

前两篇我们讲述了ReentrantLock的加锁释放锁过程,相对而言比较简单,本篇进入深水区,看看ReentrantReadWriteLock-读写锁的加锁过程是如何实现的,继续拜读老Lea凌厉的代码风。

一、读写锁的类图

读锁就是共享锁,而写锁是独占锁。读锁与写锁之间的互斥关系为:读读可同时执行(有条件的);读写与写写均互斥执行。注意此处读读可并行我用了有条件的并行,后文会对此做介绍。

继续奉上一张丑陋的类图:

可以看到ReentrantReadWriteLock维护了五个内部类,ReentrantReadWriteLock中存放了Sync、ReadLock、WriteLock三个成员变量,如下截图所示:

而ReadLock和WriteLock中又存放了Sync变量,截图如下所示,这样一组合,有了四种锁,公平读锁、公平写锁、非公平读锁、非公平写锁。对于公平与非公平的实现区别,我们上一篇已经做过讲解,本文将着重关注读锁和写锁的实现区别。

二、加锁源码

在前文中我们知道,ReentrantLock中用state来判断当前锁是否被占用,而读写锁ReentrantReadWriteLock中由于同时存在两种锁,所以老Lea用state的高16位来存放读锁的占用状态以及重入次数,低16位存放写锁的占用状态和重入次数。

1、读锁加锁,即共享锁加锁

 public void lock() {
sync.acquireShared(1); // 获取共享锁方法
}

上述lock方法中调用的获取共享锁方法是在AbstractQueuedSynchronizer中实现的,代码如下:

 public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

可以看到获取共享锁分成了两步,第一步是尝试获取,如果获取不到再进入if里面执行doAcquireShared方法,下面分别追踪。

1)、tryAcquireShared方法

 protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 1.有写锁占用并且不是当前线程,则直接返回获取失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 执行到这里,有两种情况 没有写锁占用或者是当前线程
int r = sharedCount(c); // 获取读锁次数
// 2、不应该阻塞则获取锁 @此方法有点意思,需着重讲解,作用:判断读锁是否需要阻塞
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果CAS成功,则将当前线程对应的计数+1
if (r == 0) { // 如果读锁持有数为0,则说明当前线程是第一个reader,分别给firstReader和firstReaderHoldCount初始化
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 如果读锁持有数不为0且当前线程就是firstReader,那么直接给firstReaderHoldCount+1,表示读锁重入
firstReaderHoldCount++;
} else { // 其他情况,即当前线程不是firstReader且还有其他线程持有读锁,则要获取到当前线程对应的HoldCounter,然后给里面的计数+1
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 3、应该阻塞或者CAS失败则进入此方法获取锁
return fullTryAcquireShared(current);
}

结合上述代码中的注释,将逻辑分三部分,我们一步步分析此方法的逻辑。

首先第一步,判断如果有写锁并且当前线程不是写锁的线程,则直接退出获取读锁的尝试,因为读写是互斥的,退出此方法后就会进入doAcquireShared方法,后续逻辑见下面的2)。但此处还是要看一下写锁状态统计方法exclusiveCount和读锁状态统计方法sharedCount,方法源码如下截图所示:

可以看到,exclusiveCount方法是将c和独占掩码进行与操作,独占掩码EXCLUSIVE_MASK高16位均为0,低16位均为1,按位与计算之后就剩下c的低16位,这就是第二部分一开始说的低16位存放写锁重入次数;同理看sharedCount方法,将c有符号右移16位,这样移位之后低16位就是原来的高16位,即读锁的加锁次数。老Lea通过这两个方法实现了用一个int类型的state存放写锁读锁两个加锁次数的结果,是不是看起来就很高端!

然后看第二步,判断读不应该阻塞(即readerShouldBlock方法返回false)且读锁持有次数小于最大值且CAS成功,则进入方法中尝试获取读锁。先看看重点方法readerShouldBlock什么时候会返回false(不阻塞)什么时候返回true(阻塞)。此方法在非公平模式和公平模式中有不同的实现,公平模式代码:

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

看到了一个熟悉的身影,hashQueuedPredecessors方法,这不就是在ReentrantLock中公平锁加锁时的方法么?详细可看我的AQS系列(一)中的讲解,总结一下就是该方法判断队列前面是否有在排队的非当前线程,意思就是按排队顺序获取锁,不要争抢。

非公平模式代码:

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

在后面的方法中,返回了一个四个条件组成的布尔值,逻辑为头节点不为空并且头节点后的第一个节点不为空并且这个节点是独占的并且线程不为空,此时返回true即当前这个读操作应该阻塞,不让它获取到锁。那么问题来了,为什么要有这个逻辑?此处是为了避免一种异常情况的发生,如果后面有一个排队的写锁在等待获取锁,而这时有一个读锁正在执行中,若在读锁执行完之前又来了一个读锁,因为读锁与读锁不阻塞所以后来的的读锁又获取到了锁,这时在队列第一个位置排队的写锁仍然在傻傻的等着,没办法,谁让你没关系。就这样,如果一直有读锁在当前正在执行的读锁执行完之前进来获取读锁,那么后面的写锁就会一直傻等在那,永远都没法获取锁。所以Lea就设计了这个方法来避免这种情况的发生,即如果判断队列第一位排队的是写锁,那么后面的读锁就先等一等,等这个写锁执行完了你们再执行。这也就是我在文章的开始讲的-读读同时执行是有条件的,这个条件就是指这里。

看第二步之前要先说说读锁的处理逻辑,因为是可重入的读锁,所以需要记录每个获取读锁线程的重入次数,即每个读的线程都有一个与其对应的重入次数。然后继续看第二步中读锁获取锁成功(即CAS成功)之后的逻辑:如果读锁持有数为0,则说明当前线程是第一个reader,分别给firstReader和firstReaderHoldCount初始化;如果读锁持有数不为0且当前线程就是firstReader,那么直接给firstReaderHoldCount+1,表示读锁重入;否则,即当前线程不是firstReader且还有其他线程持有读锁,则要获取到当前线程对应的HoldCounter,然后给里面的计数+1。

下面再一起看看【否则】中的逻辑,粘贴一下Sync中的部分代码

 abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
} static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
} private transient ThreadLocalHoldCounter readHolds; private transient HoldCounter cachedHoldCounter; private transient Thread firstReader = null;
private transient int firstReaderHoldCount; Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
// ...
}

可以看到,Sync中缓存了一个HoldCounter,存放的是最近一次读锁记录。而如果当前线程不是最近一次记录的HoldCounter,则去readHolds中取,readHolds是ThreadLocalHoldCounter类型,在Sync的无参构造器中初始化,它与HoldCounter都是Sync的内部类,ThreadLocalHoldCounter就是一个ThreadLocal,内部维护了一个线程与HoldCounter的键值对map,一个线程对应一个HoldCounter。所以【否则】中的逻辑加注释如下所示:

                     HoldCounter rh = cachedHoldCounter; // 获取最近一次记录的HoldCounter,此缓存是为了提高效率,不用每次都去ThreadLocal中取
if (rh == null || rh.tid != getThreadId(current)) // 判断当前线程是不是最近一次记录的HoldCounter
cachedHoldCounter = rh = readHolds.get(); // 如果不是,则去Sync中的ThreadLocal中获取,然后再放在缓存中
else if (rh.count == 0) // 如果count计数为0,说明是第一次重入,则将HoldCounter加入ThreadLocal中
readHolds.set(rh);
rh.count++; // 当前线程重入次数+1

下面进入第三步,fullTryAcquireShared方法,进入此方法的前提条件是没有写锁且 (读应该阻塞或者读锁CAS失败)。看这个full方法的逻辑:

 final int fullTryAcquireShared(Thread current) {

             HoldCounter rh = null;
for (;;) { // 无限循环直到有确定的结果返回
int c = getState();
if (exclusiveCount(c) != 0) { // 1、有独占锁且不是当前线程,直接返回读锁加锁失败
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) { // 2、判断读是否应该阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 判断如果当前线程就是firstReader,那么什么都不做,进入3中尝试获取锁,why? 因为这说明当前线程之前就持有了锁还没释放,所以可以继续获取
// assert firstReaderHoldCount > 0;
} else { // 2.5 此处逻辑需要仔细研读,乍看时看的一头雾水
if (rh == null) { // 第一次进来时rh肯定==null
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0) // 如果当前线程没获取到过读锁,则从本地线程变量中移除HoldCounter,因为下一步就要判定它获取锁失败先不让它获取了
readHolds.remove();
}
}// 能走到这里,说明当前读锁应该阻塞且不是firstReader
if (rh.count == 0) // 再加上当前线程没获取到过读锁,则先不让它尝试获取锁了,直接返回获取失败
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 3、再次尝试获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}

详细看看注解以及源代码注释、代码逻辑,相信能理解这个过程。

 2)、doAcquireShared方法

 private void doAcquireShared(int arg) {
// 将当前读锁加到队列后面
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 得到前一个节点
final Node p = node.predecessor();
if (p == head) { // 如果前一个节点是头节点,则尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) { // 设置头节点并且激活后续的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}// 判断应该挂起则挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

该方法跟之前系列中ReentrantLock的加锁过程类似,在此就不做过多的解释了,总之还是通过park来挂起。

 2、写锁加锁,即独占锁加锁

进入lock方法:

 public void lock() {
sync.acquire(1);
}

熟悉的样子,继续 点进去:

 public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

还是原先的方法,但是各个方法的实现有区别了。先看第一个tryAcquire:

 protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) { // 如果排它锁存在,则判断是不是当前线程,如果也不是当前线程,则直接返回获取失败
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
} // 判断读锁要不要阻塞,此处针对公平锁和非公平锁有不同的实现,对于非公平锁统一返回false表示不要阻塞,而公平锁则会查看前面还有没有锁来判断要不要阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

然后是addWaiter在队列末尾添加node节点排队,这个方法在AbstractQueuedSynchronizer中,同样是熟悉的方法了,此处略过不提。

最后是acquireQueued方法,如下所示,又是熟悉的代码,跟ReentrantLock中的加锁方法一毛一样,唯一的不同点是第7行调用的tryAcquire方法的实现,此处调的是ReentrantReadWriteLock类中Sync的方法,也就是上面的第一个方法。

 final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

写锁的加锁过程基本就这些了,相对来说比读锁加锁容易了很多,因为大多都跟ReentrantLock中的实现相仿。

后记

读写锁的加锁过程到此为止,最近每晚下班回来读一会,断断续续的四晚上才搞定,难受 ><

AQS系列(三)- ReentrantReadWriteLock读写锁的加锁的更多相关文章

  1. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  2. ReentrantReadWriteLock读写锁的使用2

    本文可作为传智播客<张孝祥-Java多线程与并发库高级应用>的学习笔记. 这一节我们做一个缓存系统. 在读本节前 请先阅读 ReentrantReadWriteLock读写锁的使用1 第一 ...

  3. java多线程:并发包中ReentrantReadWriteLock读写锁的原理

    一:读写锁解决的场景问题--->数据的读取频率远远大于写的频率的场景,就可以使用读写锁.二:读写锁的结构--->用state一个变量.将其转化成二进制,前16位为高位,标记读线程获取锁的次 ...

  4. ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...

  5. 锁对象-Lock: 同步问题更完美的处理方式 (ReentrantReadWriteLock读写锁的使用/源码分析)

    Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我 ...

  6. ReentrantReadWriteLock读写锁简单原理案例证明

    ReentrantReadWriteLock存在原因? 我们知道List的实现类ArrayList,LinkedList都是非线程安全的,Vector类通过用synchronized修饰方法保证了Li ...

  7. AQS系列(四)- ReentrantReadWriteLock读写锁的释放锁

    前言 继续JUC包中ReentrantReadWriteLock的学习,今天学习释放锁. 一.写锁释放锁 入口方法 public void unlock() { sync.release(1); } ...

  8. ReentrantReadWriteLock 读写锁解析

    4 java中锁是个很重要的概念,当然这里的前提是你会涉及并发编程. 除了语言提供的锁关键字 synchronized和volatile之外,jdk还有其他多种实用的锁. 不过这些锁大多都是基于AQS ...

  9. ReentrantReadWriteLock读写锁详解

    一.读写锁简介 现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁.在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源:但是如果一个线 ...

随机推荐

  1. opencv 2 Opencv数据结构与基本绘图

    基础图像容器Mat Mat 是一个类,又两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法不同,矩阵可以是不同的维数)的指针.矩阵头的尺 ...

  2. MySQL 5.7 - 通过 BINLOG 恢复数据

    日常开发,运维中,经常会出现误删数据的情况.误删数据的类型大致可分为以下几类: 使用 delete 误删行 使用 drop table 或 truncate table 误删表 使用 drop dat ...

  3. Oracle数据库 获取CLOB字段存储的xml格式字符串指定节点的值

    参照: Oracle存储过程中使用游标来批量解析CLOB字段里面的xml字符串 背景:在写存储过程时,需要获取表单提交的信息.表单信息是以xml格式的字符串存储在colb类型的字段dataxml中,如 ...

  4. Android状态栏兼容4.4.4与5.0,Android5.0状态栏由半透明设置为全透明

    //判断android 版本然后设置Systembar颜色 public void initSystemBar() { Window window = getWindow(); //4.4版本及以上 ...

  5. python3 之 变量作用域详解

    作用域: 指命名空间可直接访问的python程序的文本区域,这里的 ‘可直接访问’ 意味着:对名称的引用(非限定),会尝试在命名空间中查找名称: L:local,局部作用域,即函数中定义的变量: E: ...

  6. 【2018寒假集训 Day2】【动态规划】挖地雷

    挖地雷(Mine) 在一个地图上有N 个地窖(N<=200),每个地窖中埋有一定数量的地雷.同时,给出地窖之间的连接路径,并规定路径都是单向的,且从编号小的地窖通向编号大的地窖.某人可以从任一处 ...

  7. Linux -- 进程间通信之管道

    管道是 Linux 里的一种文件类型,同时也是 Linux 系统下进程间通信的一种方式   创建一个管道文件有两种方式:  Shell 下命令 mkfifo + filename,即创建一个有名管道 ...

  8. 浅谈集群版Redis和Gossip协议

    昨天的文章写了关于分布式系统中一致性哈希算法的问题,文末提了一下Redis-Cluster对于一致性哈希算法的实现方案,今天来看一下Redis-Cluster和其中的重要概念Gossip协议. 1.R ...

  9. PHP安装扩展补充说明

    上一篇文章中用到了,php的sodium扩展,那么如何安装PHP扩展呢?基于我之前踩过的一些坑,大致整理了几种安装php扩展的方法.已安装sodium为例 1.先做点准备工作,安装sodium依赖 r ...

  10. 【Android - 控件】之MD - NavigationView的使用

    NavigationView是Android 5.0新特性——Material Design中的一个布局控件,可以结合DrawerLayout使用,让侧滑菜单变得更加美观(可以添加头部布局). Nav ...