前言

继续JUC包中ReentrantReadWriteLock的学习,今天学习释放锁。

一、写锁释放锁

入口方法

 public void unlock() {
sync.release(1);
}

进入AQS追踪release方法:

 public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

可见跟ReentrantLock调用的同一个释放锁方法,不同点就是tryRelease方法,所以此处只看此方法即可。读写锁tryRelease方法的实现在其内部类Sync中封装,如下所示:

 protected final boolean tryRelease(int releases) {
if (!isHeldExclusively()) // 判断当前线程是不是记录的独占线程,不是的话不能释放
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0; // 判断减完之后是不是0,是0的话说明当前线程都释放了,将独占线程设置为空,后面排队的可以抢占锁了
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

跟ReentrantLock中唯一不同的地方是对于free的赋值,因为写锁的重入次数是记录在state的低16位上,所以此处要获取一下,其余的逻辑都一样。

二、读锁释放锁

 public void unlock() {
sync.releaseShared(1);
}

进入AQS追踪releaseShared方法:

 public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

只有两个关键方法tryReleaseShared和doReleaseShared,下面分别看看它们的实现逻辑。

1、tryReleaseShared

 protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {// 如果当前线程是第一个获取锁的,因为有两个成员变量直接记录,所以只要修改这两个成员变量的值即可
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // count为1,此时不应该把它置为0吗
firstReader = null;
else
firstReaderHoldCount--;
} else { // 不是当前线程,则要去缓存获取或者本地线程变量中获取当前线程的重入次数,给它减一,如果次数小于等于1则直接移除
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
} // 维护state的锁重入次数/获取次数记录
for (;;) {
int c = getState();
// 因为是读锁,所以一个SHARED_UNIT的值代表一个锁
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 如果只有读锁,这里返回什么都无所谓;所以此返回值是专门为写锁准备的,后续会根据返回值去唤醒写锁,
return nextc == 0;
}
}

该方法逻辑很清晰,for循环上面的部分代码,用户将读锁当前线程记录的重入次数-1;for循环用于将AQS中维护的state中的读锁占有次数-1.返回的布尔类型用于给后续方法判断是否要唤醒写锁。后续方法即我们下一步要追踪的doReleaseShared方法。

2、doReleaseShared

 private void doReleaseShared() {
// 唤醒后续的写线程
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 第一种:如果状态是-1,说明后面肯定有阻塞的任务,要去唤醒它
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
} // ws等于0说明是最后一个节点了,此时将Node的ws设置为PROPAGATE,因为后续没有节点了,所以不用唤醒
else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // 不等于head说明又有新的读锁进来了,这时要继续循环
break;
}
}

此方法用于唤醒读锁后处于挂起状态的锁,读锁后处于挂起状态的锁有两种:第一种是写锁,这很好理解,如果有读锁被占用,写锁过来的时候肯定需要挂起等读锁执行完(非相同线程),读锁执行完之后唤醒这个写锁;第二种是读锁,为什么当前执行的是读锁而后面还会有读锁被挂起呢?这就要回到系列(三)中的内容了,在系列(三)读锁加锁中我们讲过一个 apparentlyFirstQueuedIsExclusive 方法,该方法会判断队列中第一个排队的是不是写锁,如果是写锁则让当前的读锁挂起不去竞争锁,而若在队首写锁等待的过程中有多个读锁过来,则这多个读锁都会被依次挂起,这时就会出现第二种情况,即读锁执行的时候后面还有一个读锁被挂起,执行完之后需唤醒它。

此处第二种读锁唤醒读锁的场景,是在读锁加锁时触发的。在系列(三)中对这里未涉及,现在我们再回头看看。在doAcquireShared方法中,有个setHeadAndPropagate方法,在该方法中会检测下一个节点是不是读锁,如果是就调用doReleaseShared方法唤醒它。

小结

读写锁的释放锁逻辑基本就这些了,下面再做一个小结。

写锁释放逻辑跟ReentrantLock中的释放锁逻辑基本一致,因为毕竟都是独占锁。

读锁释放则复杂的多,它会先释放每个读锁线程记录的重入次数,再去减掉state中记录的加锁次数,最后还要唤醒后面挂起的线程。唤醒挂起的线程又分两种情况,一种是唤醒后面的写锁线程,另一种是唤醒读锁线程。读锁之间不互斥为什么在读锁执行时还会有读锁被挂起?是因为在读锁加锁时为防止写锁饥饿如果判断队首有写锁在等待获取锁那么后来的读锁都要挂起等待,这时就会出现多个读锁被挂起的情况。在释放读锁时唤醒的线程是写锁线程,在读锁加锁时唤醒的线程是读锁线程。

另外,对于Node.PROPAGATE这个状态一直没看出它的作用,而且查看了一下使用的地方,发现只在上面的doReleaseShared方法中用过,所以个人觉得是个可有可无的状态,不知道是为后续扩展留的状态还是有其他作用我没看出来,如果有对此有理解的园友,欢迎给答疑解惑一下,感谢!

AQS系列(四)- ReentrantReadWriteLock读写锁的释放锁的更多相关文章

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

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

  2. AQS系列(二)- ReentrantLock的释放锁

    前言 在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的. 正文 追踪unlock方法: public void ...

  3. ReentrantReadWriteLock读写锁详解

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

  4. ReentrantReadWriteLock读写锁的使用2

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

  5. ReentrantReadWriteLock读写锁的使用

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

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

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

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

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

  8. 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

  9. 写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

    网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...

随机推荐

  1. Linux菜鸟——常见命令一 查看及创建目录文件等命令

    命令行提示符 [root@localhost ~]# [当前用户名@主机名 当前所在目录]$ linux 超级用户 root window 超级用户 administartor # 超级用户 $ 普通 ...

  2. ArcGIS Desktop10.4安装教程

    准备内容 安装环境:win10*64位专业版 安装文件:ArcGIS_Desktop_1041_150996.iso 破解文件:10.4.1crackOnly 请都以管理员身份运行安装程序 安装步骤 ...

  3. 2011-11-14:命令执行漏洞防御,PHP反序列化漏洞产生原因,笔记

    命令执行漏洞防御尽量不要使用系统执行命令在进入执行命令函数方法之前,变量一定要做好过滤,对敏感字符进行转义在使用动态函数之前,确保使用的函数是指定的函数之一对PHP语言来说,不能完全控制的危险函数最好 ...

  4. 《手把手教你》系列练习篇之3-python+ selenium自动化测试(详细教程)

    1.  简介 前面介绍了,XPath, id , class , link text, partial link text, tag name, name 七大元素定位方法,本文介绍webdriver ...

  5. 用IDCNN和CRF做端到端的中文实体识别

    实体识别和关系抽取是例如构建知识图谱等上层自然语言处理应用的基础.实体识别可以简单理解为一个序列标注问题:给定一个句子,为句子序列中的每一个字做标注.因为同是序列标注问题,除去实体识别之外,相同的技术 ...

  6. 为什么服务器突然回复RST——小心网络中的安全设备

    RST产生原因 一般情况下导致TCP发送RST报文的原因有如下3种:    1. SYN数据段指定的目的端口处没有接收进程在等待.        2.TCP想放弃一个已经存在的连接.    3.TCP ...

  7. oracle 触发器(自增写法)

    触发器trigger 触发器我们也可以认为是存储过程,是一种特殊的存储过程. 存储过程:有输入参数和输出参数,定义之后需要调用 触发器:没有输入参数和输出参数,定义之后无需调用,在适当的时候会自动执行 ...

  8. 【开发工具 - Git】之本地项目托管到远程仓库

    这里所说的“本地项目托管到远程仓库”,说的是:例如,我们在本地有一个写了很长时间的项目,现在想要托管到GitHub或码云上进行版本控制. 这个过程大致需要以下几个步骤: (1)在本地初始化Git项目本 ...

  9. 【Android - 进阶】之Animation补间动画

    补间动画也叫View动画,它只能针对View进行动画操作,且补间动画操作的只是View中可见的部分,即只操作界面,对于可点击区域等都不会进行操作. 在Android中,补间动画的顶级类是Animati ...

  10. 环境变量PATH、cp命令、mv命令、文档查看cat/more/less/head/tail 各个命令的使用介绍

    第2周第2次课(3月27日) 课程内容: 2.10 环境变量PATH2.11 cp命令2.12 mv命令2.13 文档查看cat/more/less/head/tail 2.10 环境变量PATH P ...