aqs java 简书,Java并发之 AQS 深入解析(下)

前言

线程并发系列文章:

7a3033143802

image.png

上篇分析了AQS实现共享/独占锁的实现细节以及一些疑难点,本篇继续分析AQS剩余部分知识。通过本篇文章,你将了解到:

1、可/不可中断的独占锁

2、可/不可中断共享锁

3、可/不可限时等待的锁

4、等待/通知的实现

5、同步队列与等待队列的异同点

6、Condition.await/Condition.signal 与Object.wait/Object.notify区别

1、可/不可中断的独占锁

可中断锁的定义

打个小比喻:

先说一种场景:小明是网瘾少年,整天在网吧里打游戏,妈妈打电话叫他回家吃饭(给他发中断命令),小明口头答应了妈妈,但是一挂电话就立马又打游戏了。妈妈反复打电话,都无法中断小明的上网过程,我们说小明的上网过程是不可打断的。

再说另一种场景:小明虽然是网瘾少年,但是很听妈妈的话,妈妈打电话叫他回家吃饭(给他发中断命令),小明口头答应了并且关机了,说明小明的上网过程是可以中断的。

再来从代码角度来理解,先看一段加锁/解锁的伪代码:

private void testLock() {

myLock.lock();

doSometing1();

doSometing2();

myLock.unlock;

}

private void doSometing1() {//... }

private void doSometing2() {//... }

lock是独占锁,线程A、B同时争抢锁,假设A成功获取了锁,就能执行doSometing1()、doSometing2()方法了。

线程B调用myLock.lock()因为无法获取锁阻塞了,然而另外的线程C在等待线程B的数据,因为线程B一直拿不到锁,因此也生产不了C需要的数据,于是C想要中断B,B被唤醒后有两种选择:

1、不改初衷,继续尝试获取锁,获取不到继续阻塞。不论C怎么中断,都无动于衷。

2、检测中断是否发生了,若是发生了直接抛出异常(老子不干了,不再尝试获取锁)。

若myLock的设计满足选择1,称之为锁不可以被中断,若是满足选择2,称之为锁可被中断。

用图表示如下:

7a3033143802

image.png

不可中断的独占锁

之前分析过的独占锁的调用流程:

acquire(xx)-->acquireQueued(xx)

在acquireQueued(xx)里:

7a3033143802

image.png

如上图标注的1、2两个地方,仅仅只是检测了中断,然后将中断值返回给上一层调用者。

而上一层acquire(xx)里:

7a3033143802

image.png

发现发生过中断,也只是重新将中断标记补上而已。

可以看出,线程调用acquire(xx)获取锁,若是发生了中断,线程还是自顾自地去尝试获取锁,外界的中断调用根本无法终止它获取锁的流程。

此时的锁为:不可中断的独占锁。

可中断的独占锁

若要中断获取锁的流程,那么就需要检测中断标记位,并抛出异常或者退出流程。看看AQS是如何实现的:

#AbstractQueuedSynchronizer.java

public final void acquireInterruptibly(int arg)

throws InterruptedException {

//若是发生了中断,则抛出中断异常

if (Thread.interrupted())

throw new InterruptedException();

if (!tryAcquire(arg))

doAcquireInterruptibly(arg);

}

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);

}

}

可以看出,有两个地方抛出了异常:

1、在准备获取锁之前检测中断是否已经发生,若是则抛出异常。

2、被唤醒后检测中断是否已经发生,若是则抛出异常,中断获取锁的流程。

2、可/不可中断共享锁

不可中断的共享锁

之前分析过的共享锁的调用流程:

acquireShared(xx)-->doAcquireShared(xx)

在doAcquireShared(xx)里:

7a3033143802

image.png

与不可中断的独占锁不一样的是:doAcquireShared(xx)检测到中断后直接将中断标记位补上了,不用传递到外层了。

当然与不可中断的独占锁一样的是:它们都没有处理中断。

此时的锁为:不可中断的共享锁。

可中断的共享锁

#AbstractQueuedSynchronizer.java

public final void acquireSharedInterruptibly(int arg)

throws InterruptedException {

if (Thread.interrupted())

//若是发生了中断,则抛出中断异常

throw new InterruptedException();

if (tryAcquireShared(arg) < 0)

doAcquireSharedInterruptibly(arg);

}

private void doAcquireSharedInterruptibly(int arg)

throws InterruptedException {

final Node node = addWaiter(Node.SHARED);

boolean failed = true;

try {

for (;;) {

final Node p = node.predecessor();

if (p == head) {

int r = tryAcquireShared(arg);

if (r >= 0) {

setHeadAndPropagate(node, r);

p.next = null; // help GC

failed = false;

//因为已经在下面检测中断并抛出异常,因此此处无无需补中断

return;

}

}

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

//醒过来后,发现被中断了,于是直接抛出中断异常

throw new InterruptedException();

}

} finally {

if (failed)

cancelAcquire(node);

}

}

可以看出,与可中断的独占锁处理中断的逻辑一致。

3、可/不可限时的锁

限时等待的独占锁

由上可知,虽然锁可以响应中断,但是还有另外的场景没有覆盖到:

线程不想一直等待获取锁,而是想要等待一定的时间,若是没有获取到锁,则放弃获取锁。

来看看AQS里提供的限时等待获取锁机制:

#AbstractQueuedSynchronizer.java

public final boolean tryAcquireNanos(int arg, long nanosTimeout)

throws InterruptedException {

//传入nanosTimeout 参数,单位是纳秒,表示若是这个时间耗尽了还是没获取到锁,则退出获取锁流程

if (Thread.interrupted())

throw new InterruptedException();

return tryAcquire(arg) ||

doAcquireNanos(arg, nanosTimeout);

}

private boolean doAcquireNanos(int arg, long nanosTimeout)

throws InterruptedException {

//要求时间>0

if (nanosTimeout <= 0L)

return false;

//计算截止时间

final long deadline = System.nanoTime() + nanosTimeout;

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;

}

//查看时间是否耗尽了

nanosTimeout = deadline - System.nanoTime();

if (nanosTimeout <= 0L)

//耗尽,则直接退出

return false;

if (shouldParkAfterFailedAcquire(p, node) &&

nanosTimeout > spinForTimeoutThreshold)

//睡眠一定的时间后醒来

LockSupport.parkNanos(this, nanosTimeout);

if (Thread.interrupted())

throw new InterruptedException();

}

} finally {

if (failed)

cancelAcquire(node);

}

}

可以看出,线程挂起后,有两种方式可以将其唤醒:

1、外界调用中断方法。

2、挂起限时时间到达。

当线程被唤醒后检查中断标记位,若是发生了中断则直接抛出异常,否则再尝试获取锁,若是获取锁失败,则判断是否已经超时,若是则退出。

可限时等待的共享锁

与可限时等待的独占锁类似,不再详述。

4、等待/通知的实现

基础数据结构

AbstractQueuedSynchronizer里有个子类:ConditionObject

是实现等待/通知的基础。

先来看看其结构:

#ConditionObject

//指向队头

private transient Node firstWaiter;

//指向队尾

private transient Node lastWaiter;

firstWaiter、lastWaiter 结合Node里的nextWaiter 指针,共同维护了等待队列:

7a3033143802

image.png

数据结构有了,那么就需要操作数据结构的方法:

7a3033143802

image.png

可以看出有两种形式的方法:awaitXX(xx)/signalXX():

await():不限时等待。

await(long time, TimeUnit unit):限时等待,可以指定时间单位。

awaitNanos(long nanosTimeout):限时等待,时间单位为纳秒。

awaitUninterruptibly():不可中断的限时等待。

awaitUntil(Date deadline):限时等待,指定超时时间为将来的某个时间点。

signal():通知等待队列里节点。

signalAll():通知等待队列里所有的节点。

线程A调用awaitXX(xx)阻塞等待条件满足,线程B调用signalXX()通知线程A条件满足。显然,这就是一次线程的同步过程。

取await()/signal()/signalAll()方法来分析等待/通知机制。

ConditionObject.await() 实现

public final void await() throws InterruptedException {

//发生中断,抛出异常

if (Thread.interrupted())

throw new InterruptedException();

//加入等待队列

Node node = addConditionWaiter();//--------->(1)

//全部释放锁

int savedState = fullyRelease(node);//--------->(2)

int interruptMode = 0;

//如果不在同步队列里

while (!isOnSyncQueue(node)) {//--------->(3)

//挂起线程

LockSupport.park(this);

//线程被唤醒后,检查是否发生了中断--------->(4)

//被唤醒有两种原因:1是发生了中断,2是别的线程调用了signal

if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

break;

}

//获取同步状态

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)

//成功,则标记线程需要重新设置中断标记位

interruptMode = REINTERRUPT;

if (node.nextWaiter != null) // clean up if cancelled

//从等待队列里移除被取消的节点--------->(5)

unlinkCancelledWaiters();

//决定如何处理中断

if (interruptMode != 0)

reportInterruptAfterWait(interruptMode);//--------->(6)

}

纵观整个await()方法,主要做了3点工作:

1、封装线程到Node节点,并加入等待队列。

2、挂起线程等待条件满足。

3、线程唤醒后争抢锁。

注释标记了6个重点方法,分别来看看:

(1)

private Node addConditionWaiter() {

//找出尾节点

Node t = lastWaiter;

if (t != null && t.waitStatus != Node.CONDITION) {

//发现等待状态不是CONDITION,则遍历等待队列移除被取消的节点

unlinkCancelledWaiters();

t = lastWaiter;

}

//构造节点

Node node = new Node(Thread.currentThread(), Node.CONDITION);

if (t == null)

//若队列为空,则头指针指向当前节点

firstWaiter = node;

else

//将当前节点挂到尾节点后

t.nextWaiter = node;

//尾指针指向尾节点

lastWaiter = node;

return node;

}

该方法作用:将线程封装为节点,加入到等待队列。

(2)

既然都要等待了,那么就把锁释放掉,别的线程才能获取锁做相应的操作。

final int fullyRelease(Node node) {

boolean failed = true;

try {

//获取当前的同步状态

int savedState = getState();

//全部释放同步状态

if (release(savedState)) {

//释放成功

failed = false;

return savedState;

} else {

//释放失败,说明当前锁不是独占锁,抛出异常

throw new IllegalMonitorStateException();

}

} finally {

if (failed)

node.waitStatus = Node.CANCELLED;

}

}

从该方法可以看出:调用await()时,必须保证AQS为独占锁。

(3)

final boolean isOnSyncQueue(Node node) {

//若是当前节点状态为CONDITION 或者说node前驱节点为null,说明节点不在同步队列里

if (node.waitStatus == Node.CONDITION || node.prev == null)

return false;

//节点状态不为CONDITION 且node前驱节点存在,并且后继节点存在,则认为在同步队列里

if (node.next != null)

return true;

//后继节点不存在,可能是节点加入到同步队列尾部的时候,CAS修改tail指向不成功,因此此处再遍历同步队列直接比较节点是否相等

return findNodeFromTail(node);

}

需要保证不在同步队列里才能操作。

(4)

private int checkInterruptWhileWaiting(Node node) {

//若是发生了中断,则进一步调用transferAfterCancelledWait判断,否则直接返回0

return Thread.interrupted() ?

(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :

0;

}

final boolean transferAfterCancelledWait(Node node) {

//走到这,说明线程曾经被中断过,接下来就是要判断signal动作是否发生了

//尝试修改节点状态

if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {

//成功,则加入到同步队列里

//CAS 成功表明此时还没有发生signal()

enq(node);

return true;

}

//说明CAS失败,失败的原因是:signal()里已经将node状态改变了。

//因此,此处只需要等待signal()将节点加入到同步队列即可。

while (!isOnSyncQueue(node))

Thread.yield();

return false;

}

transferAfterCancelledWait(xx)返回true,表示需要抛出中断异常,返回false,表示只需要将中断标记位补上就好了。

interruptMode!=0,说明发生了中断,直接退出循环。

(5)

虽然前面已经将节点加入到同步队列里,但是可能并没有将节点从等待队列里移除(没有调用signal的情况),因此这里需要检测一下。

(6)

private void reportInterruptAfterWait(int interruptMode)

throws InterruptedException {

//直接抛出异常,发生了中断,但是还没有signal

if (interruptMode == THROW_IE)

throw new InterruptedException();

else if (interruptMode == REINTERRUPT)

//将中断标记位补上

selfInterrupt();

}

可以看出,即使发生了中断,await(xx)也需要等到获取锁后才会处理中断,这也保证了从await(xx)调用返回时线程一定是获取了锁。

ConditionObject.signal() 实现

public final void signal() {

if (!isHeldExclusively())

//不是独占锁,则抛出异常

throw new IllegalMonitorStateException();

//找到第一个节点

Node first = firstWaiter;

if (first != null)

doSignal(first);

}

private void doSignal(Node first) {

do {

if ( (firstWaiter = first.nextWaiter) == null)

//没有后续节点,则尾指针赋null

lastWaiter = null;

//从等待队列里移除头节点

first.nextWaiter = null;

} while (!transferForSignal(first) &&//将节点移动到同步队列

//头指针指向下一个节点

(first = firstWaiter) != null);

}

final boolean transferForSignal(Node node) {

//修改状态

if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))

return false;

//加入同步队列

Node p = enq(node);

int ws = p.waitStatus;

//p是node的前驱节点,若前驱节点被取消或者修改状态失败,则直接唤醒当前node关联的线程

if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

LockSupport.unpark(node.thread);

return true;

}

可以看出,调用signal()时需要保证AQS是独占锁,并且当前线程已经获取了独占锁。

纵观整个signal()方法,主要做了3点:

1、将节点从等待队列移除。

2、将节点加入到同步队列。

3、若同步队列只有一个节点或者修改前驱节点状态失败,则唤醒当前节点。

ConditionObject.signalAll() 实现

public final void signalAll() {

if (!isHeldExclusively())

throw new IllegalMonitorStateException();

Node first = firstWaiter;

if (first != null)

doSignalAll(first);

}

private void doSignalAll(Node first) {

lastWaiter = firstWaiter = null;

do {

Node next = first.nextWaiter;

//从等待队列移除

first.nextWaiter = null;

//加入到同步队列

transferForSignal(first);

first = next;

} while (first != null);

}

可以看出,signal()只是将等待队列头部节点移动到同步队列,而signalAll()将等待队列里的所有节点移动到同步队列。

5、同步队列与等待队列的异同点

1、同步队列是双向链表实现的(FIFO),通过Node.prev/Node.next指针链接前驱、后继节点。

2、等待队列是单向链表实现的(FIFO),通过Node. nextWaiter指针链接后继节点。

3、两者队列里的节点都是Node类型。

4、获取锁失败会加入到同步队列的队尾等待,获取锁成功将从同步队列里移除。

5、调用await()将会加入到等待队列尾部,调用signal()将从等待队列头部移除,并且加入到同步队列尾部。

6、Condition.await/Condition.signal 与Object.wait/Object.notify区别

先看一段代码:

Object.java 自带的等待/通知应用:

Object object = new Object();

private void testObjectWait() {

synchronized (object) {

try {

object.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

private void testObjectNotify() {

synchronized (object) {

object.notify();

}

}

再看Condition 等待/通知应用:

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

Condition condition2 = lock.newCondition();

private void testConditionAwait() {

lock.lock();

try {

condition.await();

} catch (InterruptedException e) {

e.printStackTrace();

} catch (Exception e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

private void testConditionSignal() {

lock.lock();

try {

condition.signal();

} catch (Exception e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

两者异同:

相同点

1、都需要获取锁后才能调用对应的方法,获取锁是为了保证条件变量在线程并发条件下的正确性。

2、等待/通知 方法成对出现。

3、等待方法都可以响应中断。

4、等待方法都支持超时返回。

不同点

Condition 等待/通知依赖AQS,也就是需要配合Lock使用,在JDK里实现。

Object 等待/通知依赖synchronized 关键字,在JVM里实现。

此外,Condition的等待/通知机制比Object的等待/通知机制更灵活,如下:

1、Condition 等待可以响应中断,也可以不响应。

2、Condition 等待可以设置超时为未来的某个时间点。

2、同一把锁可以生成多个Condition。

下一篇将会着重分析AQS 衍生的子类如:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch具体实现及其应用。

本文基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值