AQS系列(七)- 终篇:AQS总结
前言
本文是对之前AQS系列文章的一个小结,首先看看以下几个问题:
1、ReentrantLock和ReentrantReadWriteLock的可重入特性是如何实现的?
2、哪个变量控制着锁是否被占用?
3、多个线程竞争一个排它锁时,未抢到锁的线程是如何阻塞的?
4、读读真的可以一直共享不阻塞吗?
对于以上问题,你是否都能知道答案 ?是否都清楚其原理?如果是的话,就没必要阅读本文了,否则还请慢慢读来。
正文
1、可重入性的实现(针对排它锁)-问题1
可重入性是通过exclusiveOwnerThread和state一起实现的。在AQS的父类AbstractOwnableSynchronizer中有一个成员变量exclusiveOwnerThread来存放获取到独占锁时的线程,可重入的特性就是通过此变量实现的。如果根据state判断当前锁已被占用,那么再判断这个变量中存的是不是当前线程,如果是则获取到锁,锁计数+1,否则获取不到锁。(此处针对的是排它锁,共享锁不属于这个范畴)
2、AQS中state的值-问题2
state为volatile int类型,用于记录加锁状态和重入次数,但针对不同的实现类其记录方式又不同,下面分别进行说明:
【ReentrantLock】:state用于记录锁的持有状态和重入次数,state=0表示没有线程持有锁;state=1表示有一个线程持有锁;state=N表示exclusiveOwnerThread这个线程N次重入了这个锁。
【ReentrantReadWriteLock】:state用于记录读写锁的占用状态和持有线程数量(读锁)、重入次数(写锁),state的高16位记录持有读锁的线程数量,低16位记录写锁线程重入次数,如果这16位的值是0,表示没有线程占用锁,否则表示有线程持有锁。另外针对读锁,每个线程获取到的读锁次数由本地线程变量中的HoldCounter记录。
【Semaphore】:state用于计数。state=N表示还有N个信号量可以分配出去,state=0表示没有信号量了,此时所有需要acquire信号量的线程都等着;
【CountDownLatch】:state也用于计数,每次countDown都减一,减到0的时候唤醒被await阻塞的线程。
3、关于Node中waitStatus的值
waitStatus为volatile int 类型,有5种值,作用分别为:
1: CANCEL,即取消状态,这种状态的节点是无效节点,在执行时会直接略过,一般只有在特殊的异常场景中才会出现这种状态;
0:初始化状态,新建的Node节点都是这个状态;
-1:SIGNAL,即可唤醒状态,如果一个节点加锁失败需进入阻塞状态,必须先将它的前一个节点置为-1,自己才会进入park状态,在AQS中搜parkAndCheckInterrupt()可以发现,只要是这个方法出现的地方,前面一定有shouldParkAfterFailedAcquire(p, node)方法先将p的waitStatus置为-1;
-2:CONDITION,跟条件队列相关的状态,ReentrantReadWriteLock和ReentrantLock中暂未涉及;
-3:PROPAGATE,可传播状态,没看出来有什么用处。
4、关于head节点和tail节点
当第一个线程过来获取锁时,是不会初始化队列的,只是将exclusiveOwnerThread这个变量变为当前线程(独占锁的情况下),此时head和tail都是null;
第一个线程没执行完,第二个线程又来了,这时会初始化队列,new一个空Node赋值给head和tail,然后在tail(也就是head)后面拼上这个要排队的线程(详见下面的enq方法),此时形成了一个有两个节点的双向队列,head是new Node(),tail是新加入需排队的Node;然后再获取不到锁就会将head的waitStatus置为-1,自身挂起,等待unparkSuccessor(head)来唤醒。
addWaiter方法:如果tail不为空,则将当前的node节点赋值给tail,原先的tail变为新tail在链表上的前置节点。由此可知此链表的新增方式是后入式。这也解释了为什么唤醒线程时是从tail往前遍历找排在最前面符合条件的node节点。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
在AQS类中的enq方法代码如下:跟addWaiter类似,只是多了一步初始化head/tail的步骤。
1 private Node enq(final Node node) {
2 for (;;) {
3 Node t = tail;
4 if (t == null) { // Must initialize
5 if (compareAndSetHead(new Node())) // 初始化头节点,这时head指针指向的就是new Node()对象
6 tail = head; // 将tail也一起初始化了,这时会继续再走一遍for循环
7 } else {
8 node.prev = t; // 先将node的prev指针指向局部变量t;
9 if (compareAndSetTail(t, node)) { // 将tail所在内存地址的对象值换成node
10 t.next = node; // 将tail的next指针指向node节点,这时node节点就成了新的队尾了
11 return t; // 返回旧的队尾t
12 }
13 }
14 }
15 }
4、关于读读锁的有条件共享问题-问题4
ReentrantReadWriteLock中,读锁和读锁并不是永远都可以同时进行,如果当前运行的是读锁,而后面第一个排队的是写锁,那么再来新的读锁会进入链表排队阻塞而不是直接获取读锁,因为不这样设定则可能存在由于一直有读锁过来导致后面的写锁总是处于阻塞状态获取不到锁。
可参见readShouldBlock方法的非公平读锁实现-apparentlyFirstQueuedIsExclusive()方法
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
5、Object的wait/notify和LockSupport的park/unpark方法-问题3
LockSupport对线程的唤醒和挂起,是基于Thread直接信号量进行的,而wait/notify是基于对象的,需要依赖于Monitor对象。
park/unpark的语义更容易理解,park是针对当前线程阻塞,unpark是针对指定线程唤醒,不像wait/notify那样违反常识。
unpark为指定线程提供一个许可,有这个许可线程就能继续执行;而park是等待一个许可。unpark可以在park之前执行,线程不会阻塞。但unpark不能重入叠加,多个unpark也会被一个park直接用掉。
在底层,UNSAFE的park/unpark方法操作的是Paker对象(每个线程都有一个Parker实例),对象中维护了一个_counter变量,调用unpark方法后变量从0变为1,表示拥有继续执行的许可,执行之后恢复为0,调用park方法后变量如果不是1则线程阻塞,直到变为1再唤醒继续执行。
关于AQS暂时就写这些,后面还会针对JUC包里的其他工具类进行学习。
AQS系列(七)- 终篇:AQS总结的更多相关文章
- 死磕 java线程系列之终篇
(手机横屏看源码更方便) 简介 线程系列我们基本就学完了,这一个系列我们基本都是围绕着线程池在讲,其实关于线程还有很多东西可以讲,后面有机会我们再补充进来.当然,如果你有什么好的想法,也可以公从号右下 ...
- AQS系列(一)- ReentrantLock的加锁
前言 AQS即AbstractQueuedSynchronizer,是JUC包中的一个核心抽象类,JUC包中的绝大多数功能都是直接或间接通过它来实现的.本文是AQS系列的第一篇,后面会持续更新多篇,争 ...
- 跟我学SpringCloud | 终篇:文章汇总(持续更新)
SpringCloud系列教程 | 终篇:文章汇总(持续更新) 我为什么这些文章?一是巩固自己的知识,二是希望有更加开放和与人分享的心态,三是接受各位大神的批评指教,有任何问题可以联系我: inwsy ...
- jackson学习之十(终篇):springboot整合(配置类)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- JUnit5学习之八:综合进阶(终篇)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- OpenFaaS实战之九:终篇,自制模板(springboot+maven+jdk8)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- disruptor笔记之八:知识点补充(终篇)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 支持JDK19虚拟线程的web框架,之五(终篇):兴风作浪的ThreadLocal
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos <支持JDK19虚拟线程的web框架>系列 ...
- AQS系列(二)- ReentrantLock的释放锁
前言 在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的. 正文 追踪unlock方法: public void ...
随机推荐
- [BJDCTF 2nd]fake google
[BJDCTF 2nd]fake google 进入页面: 试了几下发现输入xxx,一般会按的格式显示, P3's girlfirend is : xxxxx 然后猜测会不会执行代码,发现可以执行 & ...
- angular的性能分析 -随记
$watch 的实现原理和性能分析 只有双向绑定的 scope 才会被加入$watch队列,或者手动绑定$watch的$scope 所有放在 $scope 中的变量或函数都被加入到了$watch队列当 ...
- 各种WAF绕过手法学习
原文:https://mp.weixin.qq.com/s/aeRi1lRnKcs_N2JLcZZ0Gg 0X00 Fuzz/爆破 fuzz字典 1.Seclists/Fuzzing https ...
- mysql字段数据类型、设置严格模式
表操作 今日内容 1.数据类型 建表的时候,字段都有对应的数据类型 整型 浮点型 字符类型(char与varchar) 日期类型 枚举与集合 2.约束条件 primary key unique key ...
- nopcommerce4.0 安装步骤
前言:近期因工作要求接触nopcommerce,最新版本为4.0,以下所有安装都是基于此版本.接下来我可能会写一系列,为了让自己更好的掌握,也希望能帮助到大家 好记性不如烂笔头,新手也可以避免走我的弯 ...
- [模板] dfs序
B.树之呼吸-贰之型-dfs序 Time Limit: 1000 MS Memory Limit: 32768 K Total Submit: 42 (16 users) Total Accepted ...
- 了解1D和3D卷积神经网络 | Keras
当我们说卷积神经网络(CNN)时,通常是指用于图像分类的2维CNN.但是,现实世界中还使用了其他两种类型的卷积神经网络,即1维CNN和3维CNN.在本指南中,我们将介绍1D和3D CNN及其在现实世界 ...
- ClickHouse学习系列之一【安装说明】
背景 最近花了些时间看了下ClickHouse文档,发现它在OLAP方面表现很优异,而且相对也比较轻量和简单,所以准备入门了解下该数据库系统.按照习惯第一步先安装,本文对其用户权限管理方面进行梳理说明 ...
- coding++:mybatis update foreach (SQL循环)批量更新
今天要做批量更新的业务,采用 mybaits 的 foreach 动态语句,遇到一些问题做下记录. 参考示例(1): <update id="" parameterType= ...
- 题解 P2620 虫洞
总体思路:离散化 + 建图 + 单源最短路(看见人少蒟蒻才敢发题解QAQ) 需要注意的是: 考虑到w范围较大,而实际虫洞数量较小,就只记录虫洞的起点与终点来建图. 建图时,虫洞起点可以去重. 在建图时 ...