以前我们使用 Object 类提供的线程等待/通知机制,我们先来看看一个例子:
import java.util.LinkedList;
/**
* @author : Gentle
* @date : 2019/5/20 15:08
* @description:
*/
public class Tests {
public static void main(String[] args) throws InterruptedException {
Tests tests = new Tests();
new Thread(tests::waits).start();
//让上面的线程先睡觉完毕
Thread.sleep(1000);
new Thread(tests::notifys).start();
}
//随便构建一个对象,用来当锁对象
private final Object object = new Object();
//等待的方法
private void waits() {
synchronized (object) {
try {
System.out.println("开始睡觉!");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我被唤醒啦");
}
}
//唤醒方法
private void notifys() {
synchronized (object) {
System.out.println("开始唤醒啦");
object.notifyAll();
}
}
}
上面的小例子,就是演示是 JDK 本身提供的 synchronized 关键字配合实现的等待/通知机制。下面我们开始来学习 AQS 是如何来实现等待/通知机制的!
Condition 简介:
Condition 接口可能在开发中很少遇到,但是这一点却十分重要,下面我们来看看该接口提供的方法!
ublic interface Condition {
/** 线程等待方法 **/
void await() throws InterruptedException;
/** 线程等待方法,可中断 **/
void awaitUninterruptibly();
/** 线程等待方法,有时间限定 **/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/** 线程等待方法 可指定某一个时刻**/
boolean awaitUntil(Date deadline) throws InterruptedException;
/** 唤醒一个线程 方法与 notify()类似**/
void signal();
/** 唤醒全部线程 方法与 notifyAll()类似**/
void signalAll();
}
看完接口提供的方法,来写个简单例子!
public static void main(String[] args) throws InterruptedException {
final Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//睡觉线程
new Thread(()->{
lock.lock();
try {
System.out.println("开始睡觉");
condition.await();
System.out.println("醒啦!!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
//让上面线程先完成
Thread.sleep(1000);
//唤醒线程
new Thread(()->{
lock.lock();
try {
System.out.println("准备唤醒");
condition.signal();
} finally {
lock.unlock();
}
}).start();
}
根据如上例子,现在我们有两条线程,分别为线程 A 和线程 B。线程 A 调用 await()进行阻塞,等待线程 B 将线程 A 唤醒!
首先,我们从线程 A 调用 await()方法说起。
await() 方法解析:
public final void await() throws InterruptedException {
//判断线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
//添加进等待队列中
Node node = addConditionWaiter();
//尝试释放
long savedState = fullyRelease(node);
int interruptMode = 0;
//判断当前节点状态是否正确
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//中断了
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued 是不是很熟悉?
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//节点失效了,清理失效的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
第一步是调用插入插入队列的方法 addConditionWaiter() 来看看具体实现
addConditionWaiter() 方法解析:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//构建一个新节点且 Node.CONDITION = -2
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果等待队列是空的,那将首尾指针指向当前节点
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
该方法在 AQS 类中,final 标记不允许重写!
fullyRelease()方法解析:
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;
}
}
锁释放的方法也很简单,方法很多都在上一篇文章中进行过讲述,这里就不在赘述了。
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;
}
我们先看看如下代码:
isOnSyncQueue()方法解析:
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// If has successor, it must be on queue
if (node.next != null)
return true;
return findNodeFromTail(node);
}
//找到队尾的节点
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
//当前节点是否为尾节点
if (t == node )
return true;
//尾节点
if (t == null)
return false;
t = t.prev;
}
}
isOnSyncQueue 方法主要是用于判断当前节点是否在等待队列中,如果不在等待队列中,那就开始阻塞线程!这时候线程 A 就开始了阻塞状态,等待被唤醒!
线程 B 开始调用方法唤醒
线程 B 开始工作了,线程 B 完成自己的工作后,需要唤醒线程 A,这时需要调用 signal()方法,下面我们来看看该方法实现
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)
lastWaiter = null;
//置空
first.nextWaiter = null;
//修改锁状态,并将其加入拿锁的队列中
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal()方法解析:
final boolean transferForSignal(Node node) {
//CAS 修改节点的状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//入队
Node p = enq(node);
int ws = p.waitStatus;
//状态不正确,CAS 修改状态,,然后线程阻塞
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
该方法主要是修改节点状态,将原本阻塞的线程的状态修改为最开始的状态!然后让该节点进入拿锁的等待队列中。
这时,线程池 B 的锁还没有释放,所以被唤醒的节点会先加入拿锁的等待队列中,直到线程 B 将锁释放后,在从队列中取出线程!由于这里只有两条线程 A 和 B。所以 B 线程释放锁后,A 线程便会拿到锁。
接下来还是线程 A 被唤醒后继续执行:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//被唤醒后,开始走下面代码
//尝试去拿锁,并设置状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果当前节点的下一个等待节点不为空,就将当前节点的链接清除
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
上面说的 node.nextWaiter != null,如果理解不了,那来看看具体的方法实现。
unlinkCancelledWaiters()方法解析:
private void unlinkCancelledWaiters() {
//第一个等待的节点
Node t = firstWaiter;
Node trail = null;
while (t != null) {
//拿到当前节点的下一个等待节点
Node next = t.nextWaiter;
//判断下一个节点的等待状态是否为 -2
if (t.waitStatus != Node.CONDITION) {
//置空
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
//交换,继续往下一个节点找
else
trail = t;
t = next;
}
}
如果上面都没有问题,最后走的便是线程中断了,这个不需要太多的解释了,中断方式调用的是最基本的 Thread 类中的中断方法。
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
上面讲解完毕,如果感觉还是很懵逼,我们用图的形式来进行讲解!
当我们调用 Node node = addConditionWaiter() 方法时,构建节点情况如下:
构建一个 waitStatus = -2 的节点。
将头指针和尾指针都指向当前节点!
接下来会调用 fullyRelease(node) 该方法是释放锁。Condition 接口是需要显示锁配合的,所以在调用 await() 方法前要加锁。
接下来就是线程 A 就要开始睡觉啦! LockSupport.park(this) 就是让当线程 A 去睡大觉,等着线程 B 叫醒他!
这时,线程 B 来了,执行完内部业务后开始调用 Condition.signalAll 或 Condition.signal 进行线程唤醒。
也就是上面解析过的代码:
如果等待队列中只有一个等待的线程,就直接将 lastWaiter 置空之后再将当前节点的指向下一个节点的引用断掉,调用 transferForSignal()方法将 Node 中 waitStatus 从 -2 设置为 0。
do while()将 firstWaiter 和 lastWaiter 指针置空:
将线程 A 入队,并设置成等待状态!也就是将 waitStatis 设置为 0 (阻塞状态)。
当线程 B 唤醒线程 A 时,线程 A 还是属于阻塞状态的!当线程 B 释放锁时,才是真正唤醒线程 A,线程 A 继续往下走。线程 A 会继续尝试去拿锁,
拿锁的解析在上一篇文章 ReentrantLock 中已经写过,这里就不在重复写了。
到此,等待唤醒机制已经写完了,signalAll()或者其他可中断的方式,只是在最基本的 await()基础上添加部分代码实现。
总结:
其实,说起来,AQS 的等待/通知机制和 ReentrantLock 中等待十分类似,都是将线程加入队列然后进行线程阻塞,区别是使用不同的状态参数!不过,这一套机制的实现,丰富了我们 API 的调用,也可以限时,中断等,对我们的业务也有很大帮助。
当然,有这套机制,不等于我们要忘记最基本的 wait()和 notify(),毕竟 synchronized 的实现等待/通知机制使用的就是它。
有兴趣的同学可以关注公众号,一起学习!