聊聊高并发(二十四)解析java.util.concurrent各个组件(六) 深入理解AQS(四)
最近整体过了下AQS的结构,也在网上看了一些讲AQS的文章,大部分的文章都是泛泛而谈。重新看了下AQS的代码,把一些新的要点拿出来说一说。
AQS是一个管程,提供了一个基本的同步器的能力,包含了一个状态,修改状态的原子操作,以及同步线程的一系列操作。它是CLHLock的变种,CLHLock是一个基于队列锁的自旋锁算法。AQS也采用了队列来作为同步线程的结构,它维护了两个队列,一个是作为线程同步的同步队列,另一个是基于Unsafe来进行阻塞/唤醒操作的条件队列。所以理解队列操作是理解AQS的关键。
1. 理解 head, tail引用
2. 理解 next, prev引用
3. 理解队列节点何时入队,何时出队
关于head引用,需要记住的是
1. head引用始终指向获得了锁的节点,它不会被取消。acquire操作成功就表示获得了锁,acquire过程中如果中断,那么acquire就失败了,这时候head就会指向下一个节点。
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
而获得了锁的之后,如果线程中断了,那么就需要release来释放head节点。如果线程中断了不释放锁,就有可能造成问题。所以使用显式锁时,必须要在finally里面释放锁
Lock lock = new ReentrantLock();
lock.lock();
try{
// 如果中断,可以处理获得抛出,要保证在finally里面释放锁
}finally{
lock.unlock();
}
再来看看获得锁时对head引用的处理,只有节点的前驱节点是head时,它才有可能获得锁,而获得锁之后,要把自己设置为head节点,同时把老的head的next设置为null。
这里有几层含义:
1. 始终从head节点开始获得锁
2. 新的线程获得锁之后,之前获得锁的节点从队列中出队
3. 一旦获得了锁,acquire方法肯定返回,这个过程中不会被中断
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);
}
}
关于tail引用,它负责无锁地实现一个链式结构,采用CAS + 轮询的方式。节点的入队操作都是在tail节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
next引用在队列中扮演了很重要的作用,它出现的频率很高。关于next引用,它有几种值的情况
1. next = null
2. next指向非null的下一个节点
3. next = 节点自己
next = null的情况有三种
1. 队尾节点,队尾节点的next没有显式地设置,所以为null
2. 队尾节点入队列时的上一个队尾节点next节点有可能为null,因为enq不是原子操作,CAS之后是复合操作
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
// 这个期间next可能为null
t.next = node;
return t;
}
}
}
}
3. 获取锁时,之前获取锁的节点的next设置为null
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
next指向非null的下一个节点,这种情况就是正常的在同步队列中等待的节点,入队操作时设置了前一个节点的next值,这样可以在释放锁时,通知下一个节点来获取锁
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
next指向自己,这个是取消操作时,会把节点的前一个节点指向它的后一个节点,最后把next域设置为自己
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
prev引用比较简单,它主要是维护链表结构。CLHLock是在前一个节点的状态自旋,AQS里面的节点不是在前一个状态等待,而是释放的时候由前一个节点通知队列来查找下一个要被唤醒的节点。
最后说说节点进入队列和出队列的情况。
节点入队列只有一种情况,那就是它的tryAcquire操作失败,没有获得锁,就进入同步队列等待,如果tryAcquire成功了,就不需要进入同步队列等待了。AQS提供了充分的灵活性,它提供了tryAcquire和tryRelase方法给子类扩展,基类负责维护队列操作,子类可以自己决定是否要进入队列。
所以实际子类扩展的时候有两种类型,一种是公平的同步器,一种是非公平的同步器。这里需要注意的是,所谓的非公平,不是说不使用队列来维护阻塞操作,而是说在获取竞争时,不考虑先来的线程,后来的线程可以直接竞争资源。非公平和公平的同步器竞争失败后,都需要进入AQS的同步队列进行等待,而同步队列是先来先服务的公平的队列。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
出队列有两种情况,
1. 后一个线程获得锁是,head引用指向当前获得锁的线程,前一个获得锁的节点自动出队列
2. 取消操作时,节点出队列,取消只有两种情况,一种是线程被中断,还有一种是等待超时
聊聊高并发(二十四)解析java.util.concurrent各个组件(六) 深入理解AQS(四)相关推荐
- 聊聊高并发(十七)解析java.util.concurrent各个组件(一) 了解sun.misc.Unsafe类
了解了并发编程中锁的基本原理之后,接下来看看Java是如何利用这些原理来实现各种锁,原子变量,同步组件的.在开始分析java.util.concurrent的源代码直接,首先要了解的就是sun.mis ...
- 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
这篇说说java.util.concurrent.atomic包里的类,总共12个,网上有很多文章解析这几个类,这里挑些重点说说. 这12个类可以分为三组: 1. 普通类型的原子变量 2. 数组类型的 ...
- 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一)
AQS是AbstractQueuedSynchronizer的缩写,AQS是Java并包里大部分同步器的基础构件,利用AQS可以很方便的创建锁和同步器.它封装了一个状态,提供了一系列的获取和释放操作, ...
- 聊聊高并发(二十七)解析java.util.concurrent各个组件(九) 理解ReentrantLock可重入锁
这篇讲讲ReentrantLock可重入锁,JUC里提供的可重入锁是基于AQS实现的阻塞式可重入锁.这篇 聊聊高并发(十六)实现一个简单的可重入锁 模拟了可重入锁的实现.可重入锁的特点是: 1. 是互 ...
- 聊聊高并发(三十)解析java.util.concurrent各个组件(十二) 理解CyclicBarrier栅栏
这篇讲讲CyclicBarrier栅栏,从它的名字可以看出,它是可循环使用的.它的功能和CountDownLatch类似,也是让一组线程等待,然后一起开始往下执行.但是两者还是有几个区别 1. 等待的 ...
- 聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源码分析
ThreadPoolExecutor是Executor执行框架最重要的一个实现类,提供了线程池管理和任务管理是两个最基本的能力.这篇通过分析ThreadPoolExecutor的源码来看看如何设计和实 ...
- 聊聊高并发(二十三)解析java.util.concurrent各个组件(五) 深入理解AQS(三)
这篇对AQS做一个总结. 上一篇帖了很多AQS的代码,可以看出AQS的实现思路很简单,就是提供了获取acquire和释放操作release,提供了 1. 可中断和不可中断的版本 2. 可定时和不可定时 ...
- 聊聊高并发(二十二)解析java.util.concurrent各个组件(四) 深入理解AQS(二)
上一篇介绍了AQS的基本设计思路以及两个内部类Node和ConditionObject的实现 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一) 这篇 ...
- 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore
前几篇分析了一下AQS的原理和实现,这篇拿Semaphore信号量做例子看看AQS实际是如何使用的. Semaphore表示了一种可以同时有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据, ...
- 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和主要的方法,显示了如何 ...
最新文章
- Web前端三剑客之HTML基础
- [Medical Image Processing] 2. GrayScale Histogram and Threshold-【Isodata Algorithm】
- C# 获取并判断操作系统版本,解决Win10、 Windows Server 2012 R2 读取失败的方案
- Linux下make使用gcc编译,Linux下GCC和Makefile实例(从GCC的编译到Makefile的引入)
- php生成不重复时间戳,PHP获取时间戳和微秒数以及生成唯一ID
- 复制以下命令到终端中,C++然后回车即可
- 矩阵的逆、伪逆、左右逆,最小二乘,投影矩阵
- 在Blazor中构建数据库应用程序——第3部分——UI中的CRUD编辑和查看操作
- 核心技能点-二分查找
- 3090显卡安装pytorch出错
- 如何设置计算机的网络参数,如何正确设置电脑的IP地址和DNS等参数[图文]
- 读写锁优先级的问题解决了
- Eclipse无法DEBUG
- 【软碟通 | UltraISO】刻录光盘教程(制作U盘启动盘)
- Java新手入门值得看的五本书!
- 一封没有读出来的感谢信,勾勒出蔡文胜30年创业史!
- tps在区块链是什么意思_区块链的TPS是什么
- python md5解密方法与技巧_python ---- 爬取 md5解密结果 的小脚本
- 学计算机电脑屏幕多大,买电脑显示器的技巧 电脑显示屏多大尺寸好
- 基于xc7k325t fbg900的IBRET的测试流程
热门文章
- c语言 编程显示图案*,*型图案的显示与控制(学习C语言后的编程尝试)(2)(完)...
- 安徽计算机应用基础高考试题,安徽省对口高考试题(计算机应用基础部分)
- 在表示计算机内存储器容量时 1gb等于,在表示计算机内存储器容量时,1GB等于_________MB...
- html网络,HTML—构建网络
- java class 转 字节_[转]JAVA字节数据与JAVA类型的转换
- 扫地机器人的特点描写_描写扫地机器人五年级作文500字
- java 虚拟机_浅谈Java虚拟机内存区
- controller调用controller的方法_你想过 Controller 这些方法里的参数是如何工作的吗?...
- php查询socket数据包头,php 查询数组值php中关于socket的系列函数总结
- python绘制三维曲线图_Python基于matplotlib实现绘制三维图形功能示例