AQS系列(四)- ReentrantReadWriteLock读写锁的释放锁
前言
继续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读写锁的释放锁的更多相关文章
- Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析
目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...
- AQS系列(二)- ReentrantLock的释放锁
前言 在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的. 正文 追踪unlock方法: public void ...
- ReentrantReadWriteLock读写锁详解
一.读写锁简介 现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁.在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源:但是如果一个线 ...
- ReentrantReadWriteLock读写锁的使用2
本文可作为传智播客<张孝祥-Java多线程与并发库高级应用>的学习笔记. 这一节我们做一个缓存系统. 在读本节前 请先阅读 ReentrantReadWriteLock读写锁的使用1 第一 ...
- ReentrantReadWriteLock读写锁的使用
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...
- 锁对象-Lock: 同步问题更完美的处理方式 (ReentrantReadWriteLock读写锁的使用/源码分析)
Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我 ...
- ReentrantReadWriteLock读写锁简单原理案例证明
ReentrantReadWriteLock存在原因? 我们知道List的实现类ArrayList,LinkedList都是非线程安全的,Vector类通过用synchronized修饰方法保证了Li ...
- 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!
网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...
- 写文章 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!
网上关于Java中锁的话题可以说资料相当丰富,但相关内容总感觉是一大串术语的罗列,让人云里雾里,读完就忘.本文希望能为Java新人做一篇通俗易懂的整合,旨在消除对各种各样锁的术语的恐惧感,对每种锁的底 ...
随机推荐
- 理解Spark SQL(三)—— Spark SQL程序举例
上一篇说到,在Spark 2.x当中,实际上SQLContext和HiveContext是过时的,相反是采用SparkSession对象的sql函数来操作SQL语句的.使用这个函数执行SQL语句前需要 ...
- [Odoo12基础教程]之开发过程中可能出现的问题
可能出现的问题 更改代码后无变化 当你对代码进行更改之后,发现页面并没有变化,那么请尝试依次以下几种办法: 1.重启项目: 2.升级模块: 3.在开发者模式下刷新本地模块列表: 4.给data列表添加 ...
- 2019-10-9:渗透测试,基础学习,php文件上传,mysql基础
header("Content-Type:text/html;charst="utf-8")设置头部信息,解决编码问题setcookie("loginStrin ...
- day 31 网络基础的补充
一.网络基础 1.端口 - 端口,是什么?为什么要有? 端口是为了将同一个电脑上的不同程序进行隔离. IP是找电脑 端口是找电脑上的程序 示例: MySQL是一个软件,软件帮助我们在硬盘上进行文件操作 ...
- 设计模式之工厂模式(Factory)
转载请标明出处:http://blog.csdn.net/shensky711/article/details/53348412 本文出自: [HansChen的博客] 设计模式系列文章: 设计模式之 ...
- python初识-环境搭建,变量,条件,循环语句
1.python环境搭建: (1)安装Anaconda ,可选择非C盘安装: 注意:都勾选: (2)安装Pycharm 默认安装即可,安装过程同样都勾选: (3)破解Pycharm https://w ...
- Linux基本架构
Linux linux设计思想 1.程序应该小而专一,程序应该尽量的小,且只专注于一件事上,不要开发那些看起来有用但是90%的情况都用不到的特性: 2.程序不只要考虑性能, 程序的可移植性更重要,sh ...
- VMware中windows虚拟机的安装流程
1.打开安装的VMware 15,点击新建虚拟机 2.选择典型即可,点击下一步 3.选择“稍后安装操作系统”,点击下一步 4.选择想安的版本,点击下一步 ...
- PAT(甲级)2019年秋季考试
第一题用搜索,超时了,待补 更新第一题思路 dfs + 剪枝,首先确定 n的最后一位数字肯定是9,为什么呢,因为 任意两个相邻的数肯定互为质数(gcd=1),所以 n 的末尾肯定是9,这样n+1产生的 ...
- JavaScript实现返回顶部效果
仿淘宝回到顶部效果 需求:当滚动条到一定位置时侧边栏固定在某个位置,再往下滑动到某一位置时显示回到顶部按钮.点击按钮后页面会动态滑到顶部,速度由快到慢向上滑. 思路: 1.页面加载完毕才能执行js代码 ...