谈论高并发(二十二)解决java.util.concurrent各种组件(四) 深入了解AQS(二)
上一页介绍AQS其基本设计思路以及两个内部类Node和ConditionObject实现 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一) 这篇说一说AQS的主要方法的实现。AQS和CLHLock的最大差别是,CLHLock是自旋锁,而AQS使用Unsafe的park操作让线程进入等待(堵塞)。
线程增加同步队列,和CLHLock一样,从队尾入队列,使用CAS+轮询的方式实现无锁化。
入队列后设置节点的prev和next引用,形成双向链表的结构
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;
}
}
}
}
线程指定独享还是共享方式增加队列,先尝试增加一次,假设失败再用enq()轮询地尝试,比方addWaiter(Node.EXCLUSIVE), addWaiter(Node.SHARED)
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;
}
唤醒后继节点,最典型的情况就是在线程释放锁后,会唤醒后继节点。会从节点的next開始,找到一个后继节点,假设next是null。就从队尾開始往head找,直到找到最靠近当前节点的兴许节点。 waitStatus <= 0的隐含意思是线程没有被取消。 然后用LockSupport唤醒这个找到的后继节点的线程。
这种方法类似于CLHLock里面释放锁时,通知兴许节点来获取锁。AQS使用了堵塞的方式,所以这种方法的兴许方法是acquireXXX方法,它负责将兴许节点唤醒,兴许节点再依据状态去推断是否获得锁
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
共享模式下的释放操作。从队首開始向队尾扩散,假设节点的waitStatu是SIGNAL,就唤醒后继节点。假设waitStatus是0,就设置标记成PROPAGATE
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
取消获取操作,要把节点从同步队列中去除。通过链表操作将它的前置节点的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
}
}
独占模式而且不可中断地获取队列锁的操作。这种方法在ConditionObject.await()中被使用。当线程被Unsafe.unpark唤醒后,须要调用acquireQueued来获取锁,从而结束await(). accquireQueued()方法要么获得锁,要么被tryAcquire方法抛出的异常打断,假设抛出异常,最后在finally里面取消获取
值得注意的是仅仅有节点的前驱节点是head的时候,才干获得锁。
这里隐含了一个意思,就是head指向当前获得锁的节点。当程序进入if(p == head and tryAcquire(arg))这个分支时。表示线程获得了锁或者被中断。将自己设置为head,将next设置为null.
shouldParkAfterFailedAcquired()方法的目的是将节点的前驱节点的waitStatus设置为SIGNAL,表示会通知兴许节点,这样兴许节点才干放心去park。而不用操心被丢失唤醒的通知。
parkAndCheckInterupt()方法会真正运行堵塞,并返回中断状态,这种方法有两种情况返回。一种是park被unpark唤醒。这时候中断状态为false。还有一种情况是park被中断了,因为这个accquireQueued方法是不可中断的版本号。所以即使线程被中断了,也仅仅是设置了中断标志为true,没有跑出中断异常。
在支持中断的获取版本号里。这时会抛出中断异常。
这种方法能够理解为Lock的lock里没有获取锁的分支。在CLHLock自旋锁的实现里。是对前驱节点的状态自旋,而AQS是堵塞。所以这里是在同步队列里面进入了堵塞状态。等待被前驱节点释放锁时唤醒。
释放锁时会依据状态调用unparkSuccessor()方法来唤醒兴许节点,这样就会在这种方法里面把堵塞的线程唤醒并获得锁。
队列锁的优点是线程都在多个共享状态上自旋或堵塞。所以unparkSuccessor()方法仅仅会唤醒它后继没有取消的节点。
而取消仅仅有两种情况,中断或者超时
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);
}
}
独占模式支持中断的获取队列锁操作。能够看到和不支持中断版本号的差别,这里假设parkAndCheckInterrupt()方法返回时显示被中断了,就抛出中断异常
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
独占模式限时获取队列锁操作, 这个获取的总体逻辑和前面的类似,差别是它支持限时操作,假设等待时间大于spinForTimeoutThreshold,就使用堵塞的方式等待,否则用自旋等待。使用了LockSupport.parkNanos()方法来实现限时地等待,并支持中断
这里隐含的一个含义是parkNanos方法退出有3种方式,
1. 限时到了自己主动退出,这时候会超时
2. 没有到限时被唤醒了。这时候是不超时的
3. 被中断
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime();
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
if (nanosTimeout <= 0)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享模式获得队列锁操作。获得操作也是从head的下一个节点開始,和独占模式仅仅unparkSuccessor一个节点不同,共享模式下,等head的兴许节点被唤醒了,它要扩散这样的共享的获取,使用setHeadAndPropagate操作,把自己设置为head,而且把释放的状态往下传递。这里採用了链式唤醒的方法,1个节点负责唤醒1个兴许节点,直到不能唤醒。当后继节点是共享模式isShared,就调用doReleaseShared来唤醒后继节点
doReleaseShared会从head開始往后检查状态,假设节点是SIGNAL状态,就唤醒它的后继节点。
假设是0就标记为PROPAGATE, 等它释放锁的时候会再次唤醒后继节点。
这里有个隐含的意思:
1. 增加同步队列并堵塞的节点,它的前驱节点仅仅会是SIGNAL。表示前驱节点释放锁时。后继节点会被唤醒。shouldParkAfterFailedAcquire()方法保证了这点,假设前驱节点不是SIGNAL,它会把它改动成SIGNAL。
这里不是SIGNAL就有可能是PROPAGATE
2. 造成前驱节点是PROPAGATE的情况是前驱节点获得锁时。会唤醒一次后继节点。但这时候后继节点还没有增加到同步队列,所以临时把节点状态设置为PROPAGATE,当后继节点增加同步队列后,会把PROPAGATE设置为SIGNAL,这样前驱节点释放锁时会再次doReleaseShared,这时候它的状态已经是SIGNAL了,就能够唤醒兴许节点了
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);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
tryXXXX 方法,这几个方法是给子类重写的。用来扩展响应的同步器操作
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
独占模式获取操作的顶层方法,假设没有tryAcquired,或者没有获得队列锁,就中断
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
独占模式释放操作的顶层方法,假设tryRelease()成功,那么就唤醒后继节点去获取锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
谈论高并发(二十二)解决java.util.concurrent各种组件(四) 深入了解AQS(二)的更多相关文章
- 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. watermark/2/text/aHR0cDovL2Jsb2cu ...
- 谈论高并发(三十)解析java.util.concurrent各种组件(十二) 认识CyclicBarrier栅栏
这次谈话CyclicBarrier栅栏,如可以从它的名字可以看出,它是可重复使用. 它的功能和CountDownLatch类别似,也让一组线程等待,然后开始往下跑起来.但也有在两者之间有一些差别 1. ...
- 聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源代码分析
ThreadPoolExecutor是Executor运行框架最重要的一个实现类.提供了线程池管理和任务管理是两个最主要的能力.这篇通过分析ThreadPoolExecutor的源代码来看看怎样设计和 ...
- 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore
前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票 ...
- 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...
- 线程并发线程安全介绍及java.util.concurrent包下类介绍
线程Thread,在Java开发中多线程是必不可少的,但是真正能用好的并不多! 首先开启一个线程三种方式 ①new Thread(Runnable).start() ②thread.start(); ...
- java.util.concurrent各组件分析 一 sun.misc.Unsafe
java.util.concurrent各组件分析 一 sun.misc.Unsafe 说到concurrent包也叫并发包,该包下主要是线程操作,方便的进行并发编程,提到并发那么锁自然是不可缺少的, ...
- 谈论高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference看看如何解决源代码CAS的ABA问题
于谈论高并发(十一)几个自旋锁的实现(五岁以下儿童)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用At ...
- 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁
这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,而且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了怎样模拟一个读写锁. 可重 ...
随机推荐
- ThinkPhp学习10
原文:ThinkPhp学习10 查询操作 Action模块 User下的search public function search(){ //判断username是否已经传入,且不为空 if(isse ...
- Mojo 分析日志接口
#!/usr/bin/perl #取文件行数 ##循环开始清空文件 use POSIX; use DBI; my $dir = '/data01/applog_backup'; my $file = ...
- 打造你自己ajax上传图片
今天,我们需要的图片上传插件,但是,互联网不提供符合他们的需要和易于使用的.所以我写了自己. 方法1,只使用jquery代码,.代码例如以下 <p> <label>上传图片&l ...
- [Android学习笔记]LayoutInflater的使用
LayoutInflater用于动态载入布局,然后获取到布局中定义完成的控件引用 常在动态加载布局,和Adapter中用到 使用步骤:1.通过LayoutInflater加载xml布局文件2.从载入的 ...
- 使用Seam Framework + JBoss 5.0 开发第一个Web应用 - 简单投票程序
Seam这个单词的本意是缝合.连接,因而,Seam的作用即是把Java EE 规范里的JSF 和 EJB技术完美融合在一起,免去了很多胶合代码,并增强了JSF 和 EJB的很多功能.Seam的设计目标 ...
- 【图像处理】Bilinear Image Scaling
Bilinear image scaling is about the same as nearest neighbor image scaling except with interpolation ...
- HDU1035深度搜索
/* HDU1035 意甲冠军: 给定一个字符矩阵,N S W E分别代表向上,下,剩下,进 模拟搜索,推断: 若能走出字符矩阵.则Yes,输出步数 若走不出矩阵,那么必然有圈存在,必然在矩阵中存在一 ...
- 为什么 string.find()返回值是-1
之前好像在哪里见到过这个问题,时间有点久,想不起来了,今天写字符串又碰到这个问题,书上给出的定义是当string.find()没有找到时返回的是一个非常大的值,网上有人说是-1,两种说法都对,由于整数 ...
- Android NDK入门实例 计算斐波那契数列一生成jni头文件
最近要用到Android NDK,调用本地代码.就学了下Android NDK,顺便与大家分享.下面以一个具体的实例计算斐波那契数列,说明如何利用Android NDK,调用本地代码.以及比较本地代码 ...
- 开发测试时给 Kafka 发消息的 UI 发送器――Mikasa
开发测试时给 Kafka 发消息的 UI 发送器――Mikasa 说来话长,自从入了花瓣,整个人就掉进连环坑了. 后端元数据采集是用 Storm 来走拓扑流程的,又因为 @Zola 不是很喜欢 Jav ...