文章目录
- 1. 简介
- 2. 精讲
- 1. AQS 概念
- 2. 队列同步器的接口
- 3. 队列同步器的源码分析
- 1. 同步队列
- 1. AQS 中的成员变量
- 4. 独占模式
- 1. 独占式同步状态获取:不响应中断
- 2. **独占式同步状态的释放**
- 3. 以上两点的总结
- 4. 独占式获取同步状态:可响应中断
- 5. 独占式超时获取同步状态
- 5. 共享模式
- 1. 共享式获取同步状态(不响应中断)
- 2. 共享式获取同步状态(响应中断)
- 3. 共享式超时获取同步状态
- 4. 共享式同步状态的释放
- 6. Condition 接口
- 1. Condition 接口概述
- 2. Condition 接口实现的源码分析
- 1. 等待队列
- 2. 响应线程中断等待
- 3. 不响应线程中断等待
- 4. 设置相对时间不自旋等待
- 5. 设置相对时间自旋等待
- 6. 设置绝对时间等待
- 7. 唤醒等待队列中的头节点
- 8. 唤醒等待队列中的所有节点
1. 简介
英文全称叫 AbstractQueuedSynchronizer,即队列同步器,该抽象类位于 JUC 的 locks 子包下,它是用来构建锁或者其他同步组件的基础框架,它使用了一个 volatile 修饰的整型成员变量 state 来表示同步状态,通过内置的 FIFO 队列来完成线程等待获取资源的排队工作,子类需要去继承该同步器并实现它的抽象方法进而来管理同步状态
同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件,比如:ReentrantLock 、Semaphore 、ReentrantReadWriteLock 等等,当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步组件
另外,同步器是实现锁的关键;可以这样理解二者的关系:锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;而同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理 、线程的排队 、等待和唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需要关注的领域
AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗 ?
使用者需要去继承 AQS 并重写这些指定的模板方法。将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
- isHeldExclusively() :该线程是否正在独占资源,只有用到 condition 才需要去实现它。
- tryAcquire(int) :独占方式尝试获取资源,成功则返回 true,失败则返回 false。
- tryRelease(int) :独占方式尝试释放资源,成功则返回 true,失败则返回 false。
- tryAcquireShared(int) :共享方式尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int) :共享方式尝试释放资源,成功则返回 true,失败则返回 false。
2. 精讲
【死磕Java并发】—–J.U.C之AQS:AQS简介【死磕Java并发】—–J.U.C之AQS:CLH同步队列【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放【死磕Java并发】—–J.U.C之AQS:阻塞和唤醒线程
1. AQS 概念
队列同步器 AbstractQueuedSynchronizer (简称:同步器、AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个 volatile 修饰的 int 型成员变量来表示同步状态,通过内置的 FIFO 队列来完成线程等待获取资源的排队工作。
同步器从其名字就可以看出来它是一个抽象类。因此,同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
同步状态的改变主要通过同步器提供的三个方法:getState()
、setState(int newState)
和 compareAndSetState(int expect, int update)
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用。
同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantReadWriteLock 和 CountDownLatch)。
同步器与锁的关系:同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
可以这样理解二者的关系:锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;而同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待和唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需要关注的领域
2. 队列同步器的接口
同步器的设计是基于模板方法模式的,即使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将调用使用者重写的方法。
同步器可重写的方法如下表所示:
同步器提供的模板方法如下表所示:
上表中的同步器模板方法基本上分为 3 类:独占式获取和释放同步状态 、共享式获取和释放同步状态和查询同步队列中的等待线程情况。
下面先看一个独占锁的案例:
所谓独占式,即在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后续的线程才能够获取锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 实现一个独占锁
*/
public class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer{
// 是否处于独占状态
protected boolean isHeldExclusively(){
return getState() == 1;
}
// 当状态为0时获取锁
public boolean tryAcquire(int acquires){
if(compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
protected boolean tryRelease(int releases){
if(getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition(){
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
上诉例子中,独占锁 Mutex 是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。
Mutex 中定义了一个静态内部类 Sync,该内部类继承了同步器 AQS 并实现了独占式获取(tryAcquire)和释放同步状态(tryRelease)。在 tryAcquire(int acquires)
方法中,如果经过 CAS 设置成功(同步状态设置为 1)则代表获取到了同步状态,而在 tryRelease(int releases)
方法中只是将同步状态重置为 0。
用户使用 Mutex 时并不会直接和内部同步器的实现打交道,而是调用 Mutex 提供的方法。在 Mutex 的实现中,以获取锁的 lock()
为例,只需要在方法实现中调用同步器的模板方法 acquire(int args)
即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛
3. 队列同步器的源码分析
AbstractQueuedSynchronizer 源码有一千多行,但是重复的也比较多,所以不要刚开始的时候被吓到,只要耐着性子去看慢慢的自然能够渐渐领悟。阅读 AbstractQueuedSynchronizer 源码有几个比较关键的地方需要弄明白,分别是:独占模式和共享模式的区别,节点的等待状态,以及对条件队列的理解。
下面将从 AQS 的源码分析队列同步器 AQS 是如何完成线程同步的,主要包括:同步队列 、独占式同步状态获取和释放 、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法
1. 同步队列
同步器依赖内部的同步队列(一个 FIFO 双向队列)来完成同步状态管理,当前线程获取同步状态管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱后继节点。节点的属性类型与名称以及描述如下表所示:
节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。同步队列的基本结构如下图所示:
当一个线程成功地获取了同步状态时,其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于 CAS 的设置尾节点的方法:compareAndSetTail(Node expect, Node update)
,它需要传递当前线程”认为“的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾结点建立连续
同步队列遵循 FIFO(先进先出),首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置首节点的方法并不需要使用 CAS 来保证
Node 的源码:
static final class Node {
static final Node SHARED = new Node(); // 表示当前线程以共享模式持有锁
static final Node EXCLUSIVE = null; // 表示当前线程以独占模式持有锁
// ======== 下面的几个int常量是给waitStatus用的 ===========
// 表示当前节点已经取消获取锁
//即代表此线程取消了争抢这个锁
static final int CANCELLED = 1;
// 表示后继节点的线程需要运行
//官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL = -1;
// 表示当前节点在条件队列中排队
static final int CONDITION = -2;
// 表示后继节点可以直接获取锁
static final int PROPAGATE = -3;
// 取值为上面的1、-1、-2、-3,或者0(以后会讲到)
// 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
// ps: 半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的
// 表示当前节点的等待状态
volatile int waitStatus;
// 同步队列中当前节点的前驱结点
volatile Node prev;
// 同步队列中当前结点的后继节点
volatile Node next;
// 当前节点的线程引用
// 就是线程本尊
volatile Thread thread;
// 表示条件队列中的后继节点
Node nextWaiter;
// 判断当前节点是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回当前节点的前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 构造器1:无参构造器
Node() {
}
// 构造器2:默认构造器
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 构造器3:在条件队列中使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
其中 SHARED 和 EXCLUSIVE 常量分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操作,如信号量 Semaphore 采用的就是基于 AQS 的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如 ReentranLock。
变量 waitStatus 则表示当前被封装成 Node 结点的等待状态,共有 4 种取值 CANCELLED 、SIGNAL 、CONDITION 、PROPAGATE。
- CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
- SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
- CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
- INITIAL:值为0,代表初始化状态。
1. AQS 中的成员变量
AQS 的成员变量只有三个,分别是同步队列头结点引用,同步队列尾结点引用以及同步状态。
注意,这三个成员变量都使用了 volatile 关键字进行修饰,这就确保了多个线程对它的修改都是内存可见的。整个类的核心就是这个同步状态,可以看到同步状态其实就是一个 int 型的变量,大家可以把这个同步状态看成一个密码锁,而且还是从房间里面锁起来的密码锁,state 具体的值就相当于密码控制着密码锁的开合
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
static final class Node{
// ...
}
...
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
//同步状态
// 这个是最重要的,代表当前锁的状态,0 代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;
// 获取同步状态
protected final int getState() {
return state;
}
// 设置同步状态
protected final void setState(int newState) {
state = newState;
}
// 以CAS方式设置同步状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
...
}
AbstractQueuedSynchronizer 的等待队列示意如下所示,注意了,之后分析过程中所说的 queue,也就是阻塞队列不包含 head,不包含 head,不包含 head
4. 独占模式
1. 独占式同步状态获取:不响应中断
通过调用同步器的 acquire(int arg)
方法可以获取同步状态,该方法对中断不敏感,也是由于线程获取同步状态失败后进入同步队列中,后续对该线程进行中断操作时,该线程不会从同步队列中移除
// 独占式同步状态获取与释放(不响应中断方式获取)
//为了方便理解,假如 arg 为 1
// 我们看到,这个方法,如果tryAcquire(arg) 返回 true, 也就结束了。
// 否则,acquireQueued 方法会将线程压到队列中
public final void acquire(int arg) { // 此时 arg == 1
// 首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试
// 因为有可能直接就成功了呢,也就不需要进队列排队了,
// 对于公平锁的语义就是:本来就没人持有锁,根本没必要进队列等待(又是挂起,又是等待被唤醒的)
if (!tryAcquire(arg) &&
// tryAcquire(arg) 没有成功,这个时候需要把当前线程挂起,放到阻塞队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
上诉代码主要完成了同步状态获取(即锁的获取)、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作。
其主要逻辑是:首先调用自定义同步器实现的 tryAcquire(int arg)
方法,该方法保证线程安全地获取同步状态(即获取到锁),如果失败则构造同步节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWriter(Node node)
方法将该节点加入到同步队列的尾部,最后调用 acquireQueued(Node node, int arg)
方法,使得该节点以“死循环”的方式获取同步状态(即自旋等待获取锁)
如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现
tryAcquire(int arg)
:尝试去获取同步状态 / 锁
// 尝试去获取同步状态(独占模式)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
tryAcquire(int arg)
方法需要子类去覆盖,重写里面的判断逻辑。如果获取到了同步状态则退出返回,否则生成节点加入同步队列尾部
addWaiter(Node mode)
:将当前线程包装成结点并添加到同步队列尾部
// 假设tryAcquire(arg) 返回false,那么代码将执行:
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
// 这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE)
// 即将当前线程包装成结点并添加到同步队列尾部
// 若参数mode是Node.EXCLUSIVE,则代表独占模式
// 若参数mode是Node.SHARED,则代表共享模式
private Node addWaiter(Node mode) {
// 指定持有锁的模式
Node node = new Node(Thread.currentThread(), mode);
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
// 获取同步队列尾结点引用
Node pred = tail;
// tail != null => 队列不为空(tail==head的时候,其实队列是空的)
// 如果尾结点不为空, 表明同步队列已存在结点
if (pred != null) {
// 1.指向当前尾结点
node.prev = pred;
// 2.设置当前结点为尾结点
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 3.将旧的尾结点的后继指向新的尾结点
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
// 仔细看看上面的代码,如果会到这里,
// 说明 pred==null(队列是空的) 或者 CAS失败
// (即compareAndSetTail(pred, node)为false,有线程在竞争入队)
// 即表明同步队列还没有进行初始化
enq(node);
return node;
}
// 结点入队操作
// 采用自旋的方式入队
// 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
// 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
private Node enq(final Node node) {
for (;;) {
// 获取同步队列尾结点引用
Node t = tail;
// 如果尾结点为空说明同步队列还没有初始化
// 之前说过,队列为空(即tail == null)也会进来这里
if (t == null) {
// 初始化同步队列
// 初始化head节点
// 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node())) {
// 给后面用:这个时候head节点的waitStatus==0,看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head; //初始化队列,此时队列为空
}
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上就重复排
// 1.指向当前尾结点
node.prev = t;
// 2.设置当前结点为尾结点
if (compareAndSetTail(t, node)) {
// 3.将旧的尾结点的后继指向新的尾结点
t.next = node;
return t;
}
}
}
}
上诉代码通过使用 compareAndSetTail(Node expect, Node update)
方法来确保节点能够被线程安全添加。
在 enq(final Node node)
方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过 CAS 将节点设置成为尾结点之后,当前线程才能从该方法返回,否则当前线程不断地尝试设置。
节点进入同步队列以后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自旋地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中。
执行到这一步表示,获取同步状态失败,进入同步队列中排队去了,需要说明是当前线程获取同步状态是独占模式还是共享模式,然后将这个线程挂起
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
:以独占式获取同步状态 / 锁
// 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
// 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,
// 意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false
// 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
// 独占式同步状态获取
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前结点的前驱结点的引用
final Node p = node.predecessor();
/* p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,
因为它的前驱是head。注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列,所以当前节点可以去试抢一下锁
这里我们说一下,为什么可以去试试:
首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程,也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS尝试操作一下state
(即尝试去获取一下锁)
*/
// 如果当前结点是同步队列的第一个结点, 就尝试去获取锁
if (p == head && tryAcquire(arg)) {
// 将给定结点设置为head结点
setHead(node);
// 为了帮助垃圾收集, 将上一个head结点的后继清空
p.next = null;
// 设置获取成功状态
failed = false;
// 返回中断的状态, 整个循环执行到这里才是出口
return interrupted;
}
// 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
// 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
// 所以说明锁的状态还是不可获取, 这时判断是否可以挂起当前线程
// 如果判断结果为真则挂起当前线程, 否则继续循环, 在这期间线程不响应中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()){
interrupted = true;
}
}
} finally {
// 什么时候 failed 会为 true???
// tryAcquire() 方法抛异常的情况
// 即在最后确保如果获取失败就取消获取
if (failed) {
cancelAcquire(node);
}
}
}
// 刚刚说过,会到这里就是没有抢到锁呗,这个方法说的是:当前线程没有抢到锁,是否需要挂起当前线程?
// 第一个参数是前驱节点,第二个参数才是代表当前线程的节点
// 判断是否可以将当前结点挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前继结点的等待状态
int ws = pred.waitStatus;
// 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
// 即前继结点状态为SIGNAL, 表明前驱结点会唤醒当前结点, 所以当前结点可以安心的挂起了
if (ws == Node.SIGNAL) { // 第一个分支
return true;
}
// 前驱节点 waitStatus 大于0 ,之前说过,大于0 说明前驱节点取消了排队。
// 这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
// 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
// 简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队,
// 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的
if (ws > 0) {
// 下面的操作是清理同步队列中所有已取消的前继结点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
仔细想想,如果进入到这个分支意味着什么
前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,
waitStatu都是0
正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
这里需要用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
*/
// 到这里表示前继结点状态不是SIGNAL, 很可能还是等于0,
// 这样的话前继结点就不会去唤醒当前结点了
// 所以当前结点必须要确保前继结点的状态为SIGNAL才能安心的挂起自己
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 这个方法返回 false,那么会再走一次 for 循序,
// 然后再次进来此方法,此时会从第一个分支返回 true
return false;
}
/*
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
这个方法结束根据返回值我们简单分析下:
如果返回true, 说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤
醒。我们也说过,以后是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了
如果返回false, 说明当前不需要被挂起,为什么呢?往后看
*/
// 跳回到前面是这个方法
// if (shouldParkAfterFailedAcquire(p, node) &&
// parkAndCheckInterrupt())
// interrupted = true;
// 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,
// 那么需要执行parkAndCheckInterrupt():
// 这个方法很简单,因为前面返回true,所以需要挂起线程,这个方法就是负责挂起线程的
// 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒
// 挂起当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 刚刚的线程被挂起在这里了
return Thread.interrupted();
}
// 2. 接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况
// 仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。
// 解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程:
// 是为了应对在经过这个方法后,node已经是head的直接后继节点了。剩下的读者自己想想吧
在 acquireQueued(final Node node, int arg)
方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。当头节点的线程释放了同步状态后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
整个 for 循环就只有一个出口,那就是等线程成功的获取到同步状态 / 锁之后才能出去,在没有获取到同步状态 / 锁之前就一直是挂在 for 循环的 parkAndCheckInterrupt()
方法里。线程被唤醒后也是从这个地方继续执行 for 循环。
如果我们要取消一个线程的排队,我们需要在另外一个线程中对其进行中断。比如某线程调用 lock() 老久不返回,我想中断它。一旦对其进行中断,此线程会从 LockSupport.park(this);
中唤醒,然后 Thread.interrupted();
返回 true。
我们发现一个问题,即使是中断唤醒了这个线程,也就只是设置了 interrupted = true
然后继续下一次循环。而且,由于 Thread.interrupted();
会清除中断状态,第二次进 parkAndCheckInterrupt 的时候,返回会是 false。
所以,我们要看到,在这个方法中,interrupted 只是用来记录是否发生了中断,然后用于方法返回值,其他没有做任何相关事情。
所以,我们看外层方法怎么处理 acquireQueued 返回 false 的情况。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
所以说,lock() 方法处理中断的方法就是,你中断归中断,我抢锁还是照样抢锁,几乎没关系,只是我抢到锁了以后,设置线程的中断状态而已,也不抛出任何异常出来。调用者获取锁后,可以去检查是否发生过中断,也可以不理会
从上图中可以看出,节点与节点之间的循环检查的过程中基本上不相互通信,而是简单地判断自己的前驱是否为头节点,这样使得节点的释放规则符合 FIFO
selfInterrupt()
:中断当前线程【可选项】
// 当前线程将自己中断
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
由于上面整个线程一直是挂在 for 循环的 parkAndCheckInterrupt()
方法里,没有成功获取到锁之前不会响应任何形式的线程中断,只有当线程成功获取到锁并从 for 循环出来后,才会查看在这期间是否有人要求中断线程,如果是的话再去调用 selfInterrupt()
方法将自己挂起。
独占式同步状态的获取流程,也就是 acquire(int arg)
方法的调用流程如下图所示:
当前线程获取同步状态并执行了相应逻辑后,就需要释放同步状态,使得后继节点能够继续获取同步状态。通过调用同步器的 release(int arg)
方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点
2. 独占式同步状态的释放
public final boolean release(int arg) {
// 拨动密码锁, 看看是否能够开锁
if (tryRelease(arg)) {
// 获取head结点
Node h = head;
// 如果head结点不为空并且等待状态不等于0就去唤醒后继结点
if (h != null && h.waitStatus != 0) {
// 唤醒后继结点
unparkSuccessor(h);
}
return true;
}
return false;
}
// 唤醒后继结点
// 从上面调用处知道,参数node是head头结点
private void unparkSuccessor(Node node) {
// 获取给定结点的等待状态
int ws = node.waitStatus;
// 如果head节点当前waitStatus<0, 将其修改为0
// 将等待状态更新为0
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
// 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
// 获取当前结点的后继结点
Node s = node.next;
// 后继结点为空或者等待状态为取消状态
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找,仔细看代码,不必担心中间有节点取消(waitStatus==1)的情况
// 从后向前遍历队列找到第一个不是取消状态的结点
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
// 唤醒给定结点后面首个不是取消状态的结点
if (s != null) {
//唤醒线程
LockSupport.unpark(s.thread);
}
}
该方法执行时会唤醒头节点的后继节点线程,unparkSuccessor(Node node)
方法使用 LockSupport 来唤醒处于等待状态的线程。
3. 以上两点的总结
以上分析了独占式同步状态的获取和释放过程,做个总结:
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg)
方法释放同步状态,然后唤醒头节点的后继节点
一个关于获取锁和释放锁的案例:
// 用一个web开发中的service概念
public class OrderService {
// 使用static,这样每个线程拿到的是同一把锁,当然,spring mvc中service默认就是单例,别纠结这个
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public void createOrder() {
// 比如我们同一时间,只允许一个线程创建订单
reentrantLock.lock();
// 通常,lock 之后紧跟着 try 语句
try {
// 这块代码同一时间只能有一个线程进来(获取到锁的线程),
// 其他的线程在lock()方法上阻塞,等待获取到锁,再进来
// 执行代码...
// 执行代码...
// 执行代码...
} finally {
// 释放锁
reentrantLock.unlock();
}
}
}
首先,第一个线程调用 reentrantLock.lock(),翻到最前面可以发现,tryAcquire(1) 直接就返回 true 了,结束。只是设置了 state=1,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,那世界就太太太平了,完全没有交集嘛,那我还要 AQS 干嘛。
如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?
线程 2 会初始化 head【new Node()】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)
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;
}
}
}
}
首先,是线程 2 初始化 head 节点,此时 headtail, waitStatus0
然后线程 2 入队:
同时我们也要看此时节点的 waitStatus,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus 没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire 这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus设置为 -1。
那线程 2 节点此时的 waitStatus 是多少呢,由于没有设置,所以是 0;
如果线程 3 此时再进来,直接插到线程 2 的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。
这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码
4. 独占式获取同步状态:可响应中断
上诉过程是独占式获取同步状态的过程,这个过程是不响应线程的中断的。那怎样响应线程中断获取同步状态呢?
// 以可中断模式获取同步状态 / 锁(独占模式)
private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 将当前线程包装成结点添加到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
// 获取当前结点的前继结点
final Node p = node.predecessor();
// 如果p是head结点, 那么当前线程就再次尝试获取锁
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);
}
}
}
响应线程中断方式和不响应线程中断方式获取锁流程上大致上是相同的。唯一的一点区别就是线程从 parkAndCheckInterrupt
方法中醒来后会检查线程是否中断,如果是的话就抛出 InterruptedException 异常,而不响应线程中断获取锁是在收到中断请求后只是设置一下中断状态(设置为 true),并不会立马结束当前获取锁的方法,一直到结点成功获取到锁之后才会根据中断状态决定是否将自己挂起
5. 独占式超时获取同步状态
通过调用同步器的 doAcquireNanos(int arg, long nanosTimeout)
方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回 true,否则返回 false。该方法提供了传统 Java 同步操作(比如 synchronized 关键字)所不具备的特性。
针对超时获取,主要需要计算出需要睡眠的时间间隔 nanosTimeout,为了防止过早通知,nanosTimeout 计算公式为:nanosTimeout -= now - lastTime
,其中 now 为当前唤醒时间,lastTime 为上次唤醒时间,如果 nanosTime 大于 0 则表示超时时间未到,需要继续睡眠 nanosTimeout (结果值)纳秒,反之,则已经超时
// 以限定超时时间获取同步状态/ 锁(独占模式)
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();
//如果前继是head结点, 那么当前线程就再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 更新head结点
setHead(node);
p.next = null;
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
lastTime = now;
// 在获取锁的期间收到中断请求就抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从方法中返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout 小于等于 0,就代表已经超时了),如果没有超时,重新计算 nanosTimeout,然后使当前线程等待 nanosTimeout 纳秒(当已到设置的超时时间,该线程会从 LockSupport.parkNanos(Object blocker, long nanos)
方法返回)
注意在以超时时间获取锁的过程中是可以响应线程中断请求的
从上图可以看出:独占式超时获取同步状态 doAcquireNanos(int arg, long nanosTimeout)
和独占式获取同步状态 acquire(int args)
在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)
在未获取同步状态时,将会使当前线程一直处于等待状态,而 doAcquireNanos(int arg, long nanosTimeout)
会使当前线程等待 nanosTimeout 纳秒,如果当前线程在 nanosTimeout 纳秒没有获取到同步状态,将会从等待逻辑中自动返回
5. 共享模式
通过上一篇文章的的分析,我们知道独占模式获取同步状态(或者说获取锁)有三种方式,分别是:独占式同步状态获取(不响应中断)、独占式同步状态获取(响应中断)和超时获取同步状态。
在共享模式下获取同步状态的方式也是这三种,而且基本上大同小异,理解其中一种很快就能弄明白另一种了。
共享模式获取同步状态与独占模式获取同步状态最主要的区别在于同一时刻能否有多个线程同时获取同步状态。以文件为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作可以同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问
1. 共享式获取同步状态(不响应中断)
通过调用同步器的 acquireShared(int arg)
方法可以共享式地获取同步状态。该方法的源码如下:
public final void acquireShared(int arg) {
// 1.尝试去获取同步状态
if (tryAcquireShared(arg) < 0) {
// 2.如果获取失败就进入这个方法
doAcquireShared(arg);
}
}
// 尝试去获取同步状态(共享模式)
// 负数:表示获取失败
// 零值:表示当前结点获取成功, 但是后继结点不能再获取了
// 正数:表示当前结点获取成功, 并且后继结点同样可以获取成功
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
调用 acquireShared()
方法是不响应线程中断共享式获取同步状态的方式。在该方法中,首先调用 tryAcquireShared()
去尝试获取同步状态,tryAcquireShared()
方法返回一个获取同步的状态。这里 AQS 规定了返回状态若是负数代表当前结点获取同步状态失败,若是 0 代表当前结点获取同步状态成功,但后继结点不能再获取了,若是正数则代表当前结点获取同步状态成功,并且这个同步状态后续结点也同样可以获取成功。
自定义子类在实现 tryAcquireShared
方法获取同步状态的逻辑时,返回值需要遵守这个约定。如果调用tryAcquireShared
的返回值小于 0,就代表这次尝试获取同步状态失败了,接下来就调用 doAcquireShared
方法将当前线程添加进同步队列
private void doAcquireShared(int arg) {
// 添加到同步队列中,指定节点为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前结点的前继结点
final Node p = node.predecessor();
// 如果前继结点为head结点就再次尝试去获取锁
if (p == head) {
// 再次尝试去获取同步状态并返回获取状态
// r < 0, 表示获取失败
// r = 0, 表示当前结点获取成功, 但是后继结点不能再获取了
// r > 0, 表示当前结点获取成功, 并且后继结点同样可以获取成功
int r = tryAcquireShared(arg);
if (r >= 0) {
// 到这里说明当前结点已经获取同步状态成功了, 此时它会将锁的状态信息传播给后继结点
setHeadAndPropagate(node, r);
p.next = null;
// 如果在线程阻塞期间收到中断请求, 就在这一步响应该请求
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
// 每次获取锁失败后都会判断是否可以将线程挂起,
// 如果可以的话就会在parkAndCheckInterrupt方法里将线程挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
进入 doAcquireShared
方法首先是调用 addWaiter
方法将当前线程包装成节点放到同步队列尾部。这个添加节点的过程我们在讲独占模式时讲过,这里就不再讲了。
节点进入同步队列后,如果它发现在它前面的节点就是 head 结点,因为 head 结点的线程已经获取同步状态了,那么下一个获取同步状态的节点就轮到自己了,所以当前节点先不会将自己挂起,而是再一次去尝试获取同步状态,如果前面的节点刚好释放锁离开了,那么当前节点就能成功获得锁。
如果前面那个节点还没有释放锁,那么就会调用 shouldParkAfterFailedAcquire
方法,在这个方法里面会将 head 节点的状态改为 SIGNAL,只有保证前面节点的状态为 SIGNAL,当前节点才能放心的将自己挂起,所有线程都会在 parkAndCheckInterrupt
方法里面被挂起。
如果当前节点恰巧成功的获取了锁,那么接下来就会调用 setHeadAndPropagate
方法将自己设置为 head 节点,并且唤醒后面同样是共享模式的节点。下面我们看下 setHeadAndPropagate
方法具体的操作
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将给定结点设置为head结点
setHead(node);
// 如果propagate大于0表明锁可以获取了
if (propagate > 0 || h == null || h.waitStatus < 0) {
// 获取给定结点的后继结点
Node s = node.next;
// 如果给定结点的后继结点为空, 或者它的状态是共享状态
if (s == null || s.isShared()) {
// 唤醒后继结点
doReleaseShared();
}
}
}
//释放同步状态的操作(共享模式)
private void doReleaseShared() {
for (;;) {
// 获取同步队列的head结点
Node h = head;
if (h != null && h != tail) {
// 获取head结点的等待状态
int ws = h.waitStatus;
// 如果head结点的状态为SIGNAL, 表明后面有人在排队
if (ws == Node.SIGNAL) {
// 先把head结点的等待状态更新为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
// 再去唤醒后继结点
unparkSuccessor(h);
// 如果head结点的状态为0, 表明此时后面没人在排队, 就只是将head状态修改为PROPAGATE
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
// 只有保证期间head结点没被修改过才能跳出循环
if (h == head) {
break;
}
}
}
调用 setHeadAndPropagate
方法首先将自己设置成 head 节点,然后再根据传入的 tryAcquireShared
方法的返回值来决定是否要去唤醒后继节点。
前面已经讲到当返回值大于 0 就表明当前节点成功获取了同步状态,并且后面的节点也可以成功获取同步状态。这时当前节点就需要去唤醒后面同样是共享模式的节点,注意,每次唤醒仅仅只是唤醒后一个节点,如果后一个节点不是共享模式的话,当前节点就不会再去唤醒更后面的节点了。
共享模式下唤醒后继节点的操作是在 doReleaseShared
方法进行的,共享模式和独占模式的唤醒操作基本也是相同的,都是去找到自己座位上的牌子(等待状态),如果牌子上为 SIGNAL 表明后面有人需要让它帮忙唤醒,如果牌子上为 0 则表明队列此时并没有节点在排队。
在独占模式下是如果发现没节点在排队就直接离开队列了,而在共享模式下如果发现队列后面没节点在排队,当前节点在离开前仍然会留个小纸条(将等待状态设置为 PROPAGATE )告诉后来的人这个锁的可获取状态。那么后面来的人在尝试获取锁的时候可以根据这个状态来判断是否直接获取锁
2. 共享式获取同步状态(响应中断)
响应中断操作的共享模式下获取同步状态是调用同步器中的 acquireSharedInterruptibly(int arg)
方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 首先判断线程是否中断, 如果是则抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 1.尝试去获取同步状态
if (tryAcquireShared(arg) < 0) {
// 2. 如果获取失败则进人该方法
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) {
// 1.如果node的前驱结点是头节点,则node尝试获取同步状态
int r = tryAcquireShared(arg);
// 2.如果获取到同步状态
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
// 如果线程在阻塞过程中收到过中断请求, 那么就会立马在这里抛出异常
throw new InterruptedException();
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
共享模式下响应线程中断获取同步状态的方式和不响应线程中断获取同步状态的方式在流程上基本是相同的,唯一的区别就是在哪里响应线程的中断请求。
在不响应线程中断获取同步状态时,线程从 parkAndCheckInterrupt
方法中被唤醒,唤醒后就立马返回是否收到中断请求,即使是收到了中断请求也会继续自旋直到获取同步状态后才响应中断请求将自己给挂起。而响应线程中断获取同步状态会在线程被唤醒后立马响应中断请求,如果在阻塞过程中收到了线程中断就会立马抛出 InterruptedException 异常
3. 共享式超时获取同步状态
共享式超时获取同步状态需要调用同步器 AQS 抽象类中的 tryAcquireSharedNanos(int arg, long nanosTimeout)
方法,该方法的调用过程如下:
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 1.调用tryAcquireShared尝试去获取同步状态
// 2.如果获取失败就调用doAcquireSharedNanos
return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
}
// 以限定超时时间获取同步状态(共享模式)
private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
long lastTime = System.nanoTime();
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 判断当前结点的前驱节点是否是头节点
if (p == head) {
// 如果是头节点,则尝试获取同步状态,并返回int状态值
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null;
failed = false;
return true;
}
}
// 如果超时时间用完了就结束获取, 并返回失败信息
if (nanosTimeout <= 0) {
return false;
}
// 1.检查是否满足将线程挂起要求(保证前继结点状态为SIGNAL)
// 2.检查超时时间是否大于自旋时间
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);
}
}
}
如果看懂了上面两种获取方式,再来看设置超时时间的获取方式就会很轻松,基本流程都是一样的,主要是理解超时的机制是怎样的。
如果第一次获取同步状态失败会调用 doAcquireSharedNanos
方法并传入超时时间,进入方法后会根据情况再次去获取同步状态,如果再次获取失败就要考虑将线程挂起了。
这时会判断超时时间是否大于自旋时间,如果是的话就会将线程挂起一段时间,否则就继续尝试获取,每次获取锁之后都会将超时时间减去获取同步状态的时间,一直这样循环直到超时时间用尽,如果还没有获取到锁的话就会结束获取并返回获取失败标识。在整个期间线程是响应线程中断的
4. 共享式同步状态的释放
与独占式一样,共享式获取也需要释放同步状态,通过调用 realeaseShared(int arg)
方法可以释放同步状态,源码分析如下:
public final boolean releaseShared(int arg) {
// 1.尝试去释放同步状态
if (tryReleaseShared(arg)) {
// 2.如果释放成功就唤醒其他线程
doReleaseShared();
return true;
}
return false;
}
// 尝试去释放同步状态(共享模式)
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 释放同步状态的操作(共享模式)
private void doReleaseShared() {
for (;;) {
// 获取同步队列的head结点
Node h = head;
if (h != null && h != tail) {
// 获取head节点的等待状态
int ws = h.waitStatus;
// 如果head节点的状态为SIGNAL, 表明后面有人在排队
if (ws == Node.SIGNAL) {
// 先把head节点的等待状态更新为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
// 再去唤醒后继结点
unparkSuccessor(h);
// 如果head节点的状态为0, 表明此时后面没人在排队, 就只是将head状态修改为PROPAGATE
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
// 只有保证期间head结点没被修改过才能跳出循环
if (h == head) {
break;
}
}
}
获取同步状态(或者说锁)的线程执行完逻辑后就会调用 releaseShared
方法释放同步状态,首先调用tryReleaseShared
方法尝试释放同步状态,该方法的判断逻辑由自定义子类通过覆盖它来实现的。
如果释放成功就调用 doReleaseShared
方法去唤醒后继结点。后它会找到原先的座位(head 结点),看看座位上是否有节点留了小纸条(状态为 SIGNAL),如果有就去唤醒后继结点。
如果没有(状态为 0)就代表队列没节点在排队,那么在离开之前它还要做最后一件事情,就是在自己座位上留下小纸条(状态设置为 PROPAGATE),告诉后面的节点锁的获取状态。
整个释放锁的过程和独占模式唯一的区别就是在这最后一步操作。因为共享式释放同步状态的操作可能会同时来自多个线程,为了保证同步状态线程安全释放,一般是通过循环和 CAS 保证的
6. Condition 接口
AbstractQueuedSynchronizer 内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列和等待队列
要学习条件队列,就必须先了解 Condition 接口
1. Condition 接口概述
每个 Java 对象都拥有一组监视器方法,主要包括:wait() 、wait(long timeout )、notify() 和 notifyAll() 方法,这些方法与 synchronized 关键字配合,可以实现等待 / 通知模式。
Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待 / 通知模式,但是这两者在使用方法和功能特性上还是有差别的
Condition 定义了 等待 / 通知 的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。Condition 对象是由 Lock 对象(调用 Lock 对象的 newCondition() 方法)创建出来的,即 Condition 对象是依赖 Lock 对象的。
Condition 的使用方式比较简单,需要注意在调用方法前获取锁,案例代码如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionUseCase {
Lock lock = new ReentrantLock();
// 通过lock.newCondition()方法创建condition对象
// 即Condition 依赖于Lock 来创建
Condition condition = lock.newCondition();
// 等待:conditionWait()
public void conditionWait() throws InterruptedException {
lock.lock();
try {
//当前线程进入等待状态
condition.await();
} finally {
}
}
// 通知:conditionSignal()
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
// 唤醒一个等待在Condition上的线程
condition.signal();
} finally {
lock.unlock();
}
}
}
一般会将 Condition 对象作为成员变量。当调用 await() 方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal() 方法,通知当前线程后,当前线程才从 await() 方法返回,并且在返回前已经获取了锁。
Condition 的主要方法如下表所示:
Condition 源码:
public interface Condition {
// 响应线程中断的条件等待
void await() throws InterruptedException;
// 不响应线程中断的条件等待
void awaitUninterruptibly();
// 设置相对时间的条件等待(不进行自旋)
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 设置相对时间的条件等待(进行自旋)
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 设置绝对时间的条件等待
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒条件队列中的头节点
void signal();
// 唤醒条件队列的所有节点
void signalAll();
}
Condition 接口虽然定义了这么多方法,但总共就分为两类,以 await 开头的是线程进入条件队列等待的方法,以 signal 开头的是将条件队列中的线程“唤醒”的方法。这里要注意的是,调用 signal 方法可能唤醒线程也可能不会唤醒线程,什么时候会唤醒线程这得看具体情况。但是调用 signal 方法一定会将线程从条件队列中移到同步队列尾部。
await 方法分为5种,分别是:响应线程中断等待、不响应线程中断等待、设置相对时间不自旋等待、设置相对时间自旋等待、设置绝对时间等待;
signal 方法只有2种,分别是:只唤醒条件队列头节点和唤醒条件队列所有节点
2. Condition 接口实现的源码分析
ConditionObject 是同步器 AQS 的内部类,它实现了 Condition 接口。每个 Condition 对象都包含着一个队列(以下称之为等待队列),该队列是 Condition 对象是实现 等待/通知 机制的关键。
下面将分析 Condition 的实现,主要包括:等待队列、等待和通知,下面提到的 Condition 如果不加说明均指的是 ConditionObject
1. 等待队列
等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在 Condition 对象上等待的线程,如果一个线程调用了 Condition.await()
方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。这里的用的节点类型也是同步器的静态内部类 Node
这里举个例子,便于帮助理解等待队列:
我们拿公共厕所做比喻,同步队列是主要的排队区,如果公共厕所没开放,所有想要进入厕所的人都得在这里排队。而等待队列主要是为条件等待设置的,我们想象一下如果一个人通过排队终于成功获取锁进入了厕所,但在方便之前发现自己没带手纸,碰到这种情况虽然很无奈,但是它也必须接受这个事实,这时它只好乖乖的出去先准备好手纸(进入等待队列等待),当然在出去之前还得把锁给释放了好让其他人能够进来,在准备好了手纸(条件满足)之后它又得重新回到同步队列中去排队。
当然进入等待队列的人并不都是因为没带手纸,可能还有其他一些原因必须中断操作先去等待队列中去排队,所以等待队列可以有多个,按照不同的等待条件而设置不同的等待队列。等待队列是一条单向链表,Condition接口定义了等待队列中的所有操作,AbstractQueuedSynchronizer内部的ConditionObject类实现了Condition接口
一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用 Condition.await()
方法,将会把当前线程封装成节点,并将节点从尾部加入等待队列
Condition 拥有尾结点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上诉节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await() 方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包 JUC 中的 Lock(更确切的说是同步器)拥有一个同步队列和多个等待队列
基本上,把这张图看懂,你也就知道 condition 的处理流程了。所以,我先简单解释下这图,然后再具体地解释代码实现。
- 条件队列和阻塞队列的节点,都是 Node 的实例,因为条件队列的节点是需要转移到阻塞队列中去的;
- 我们知道一个 ReentrantLock 实例可以通过多次调用 newCondition() 来产生多个 Condition 实例,这里对应 condition1 和 condition2。注意,ConditionObject 只有两个属性 firstWaiter 和 lastWaiter;
- 每个 condition 有一个关联的条件队列,如线程 1 调用
condition1.await()
方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行,条件队列是一个单向链表; - 调用
condition1.signal()
触发一次唤醒,此时唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter(队头) 移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行。
上面的 2->3->4 描述了一个最简单的流程,没有考虑中断、signalAll、还有带有超时参数的 await 方法等,不过把这里弄懂是这节的主要目的。
同时,从图中也可以很直观地看出,哪些操作是线程安全的,哪些操作是线程不安全的
2. 响应线程中断等待
调用 Condition 的 await() 方法,会使当前线程进入等待队列并释放锁,同时线程状态转变为等待状态。当从 await() 方法返回时,当前线程一定获取了 Condition 相关联的锁。
如果从队列的角度看 await() 方法,当调用 await() 方法时,相当于同步队列的首节点(获取了同步状态 / 锁的节点)移动到 Condition 的等待队列中。
下面看下 ConditionObject 的 await() 方法的源码:
// 响应线程中断的条件等待
// 这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll(),下同),或被中断
public final void await() throws InterruptedException {
// 老规矩,既然该方法要响应中断,那么在最开始就判断中断状态
// 如果线程被中断则抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 添加到 condition 的条件队列中
// 将当前线程添加到条件队列尾部
Node node = addConditionWaiter();
// 释放锁,返回值是释放锁之前的 state 值
// await() 之前,当前线程是必须持有锁的,这里肯定要释放掉
// 在进入条件等待之前先完全释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 线程一直在while循环里进行条件等待
// 这里退出循环有两种情况,之后再仔细分析
// 1. isOnSyncQueue(node) 返回 true,即当前 node 已经转移到阻塞队列了
// 2. checkInterruptWhileWaiting(node) != 0 会到 break,然后退出循环,代表的是线程中断
while (!isOnSyncQueue(node)) {
// 进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:
// 1.同步队列的前驱节点已取消
// 2.设置同步队列的前驱节点的状态为SIGNAL失败
// 3.前驱节点释放锁后唤醒当前节点
LockSupport.park(this);
// 当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待, 此时需要将结点移出条件队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
// 被唤醒后,将进入阻塞队列,等待获取锁
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 这步操作主要为防止线程在signal之前中断而导致没与条件队列断绝联系
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
}
当线程调用 await() 方法的时候,首先会将当前线程包装成节点放入等待队列的尾部。在 addConditionWaiter()
方法中,如果发现等待队列尾结点已取消就会调用 unlinkCancelledWaiters()
方法将条件队列所有的已取消结点清空。
这步操作是插入结点的准备工作,那么确保了尾结点的状态也是 CONDITION 之后,就会新建一个节点将当前线程包装起来然后放入等待队列尾部。注意,这个过程只是将节点添加到同步队列尾部而没有挂起线程。
addConditionWaiter()
是将当前节点加入到条件队列,看图我们知道,这种条件队列内的操作是线程安全的
// 将当前线程对应的节点入队,插入队尾
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果条件队列的最后一个节点取消了,将其清除出去
// 为什么这里把 waitStatus 不等于 Node.CONDITION,就判定为该节点发生了取消排队?
if (t != null && t.waitStatus != Node.CONDITION) {
// 这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列
unlinkCancelledWaiters();
t = lastWaiter;
}
// node 在初始化的时候,指定 waitStatus 为 Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t 此时是 lastWaiter,队尾
// 如果队列为空
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
unlinkCancelledWaiters()
方法,该方法用于清除队列中已经取消等待的节点
当 await 的时候如果发生了取消操作,或者是在节点入队的时候,发现最后一个节点是被取消的,会调用一次这个方法
// 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去
// 纯属链表操作,很好理解,看不懂多看几遍就可以了
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的
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;
}
}
下面看下释放锁的 fullyRelease()
方法的源码:
考虑一下这里的 savedState。如果在 condition1.await() 之前,假设线程先执行了 2 次 lock() 操作,那么 state 为 2,我们理解为该线程持有 2 把锁,这里 await() 方法必须将 state 设置为 0,然后再进入挂起状态,这样其他线程才能持有锁。当它被唤醒的时候,它需要重新持有 2 把锁,才能继续下去
// 完全释放锁
// 首先,我们要先观察到返回值 savedState 代表 release 之前的 state 值
// 对于最简单的操作:先 lock.lock(),然后 condition1.await()。
// 那么 state 经过这个方法由 1 变为 0,锁释放,此方法返回 1
// 相应的,如果 lock 重入了 n 次,savedState == n
// 如果这个方法失败,会将节点设置为"取消"状态,并抛出异常 IllegalMonitorStateException
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取当前的同步状态
int savedState = getState();
// 使用当前的同步状态去释放锁
// 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
if (release(savedState)) {
failed = false;
// 如果释放锁成功就返回当前同步状态
return savedState;
} else {
// 如果释放锁失败就抛出运行时异常,包含不持有锁的情况(不持有锁也就无法释放锁)
throw new IllegalMonitorStateException();
}
} finally {
// 保证没有成功释放锁就将该节点设置为取消状态
if (failed) {
node.waitStatus = Node.CANCELLED;
}
}
}
将当前线程包装成结点添加到等待队列尾部后,紧接着就调用 fullyRelease()
方法释放锁。注意,方法名为fullyRelease
也就是这步操作会完全的释放锁,因为锁是可重入的,所以在进行条件等待前需要将锁全部释放了,不然的话别人就获取不了锁了。如果释放锁失败的话就会抛出一个运行时异常,如果成功释放了锁的话就返回之前的同步状态。
注意:如果一个线程在不持有 lock 的基础上,就去调用 condition1.await() 方法,它能进入条件队列,但是在上面的这个方法中,由于它不持有锁,release(savedState) 这个方法肯定要返回 false,进入到异常分支,然后进入 finally 块设置 node.waitStatus = Node.CANCELLED
,这个已经入队的节点之后会被后继的节点”请出去“。
下面来看看进行条件等待的源码:
int interruptMode = 0;
// 如果不在阻塞队列中,注意了,是阻塞队列
// 线程一直在while循环里进行条件等待
while (!isOnSyncQueue(node)) {
// 进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:
// 1.同步队列的前继结点已取消
// 2.设置同步队列的前继结点的状态为SIGNAL失败
// 3.前继结点释放锁后唤醒当前结点
LockSupport.park(this);
// 当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待, 此时需要将结点移出等待队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
/*
先解释下 interruptMode。interruptMode 可以取值为 REINTERRUPT(1),THROW_IE(-1),0
REINTERRUPT: 代表 await 返回的时候,需要重新设置中断状态
THROW_IE: 代表 await 返回的时候,需要抛出 InterruptedException 异常
0 :说明在 await 期间,没有发生中断
有以下三种情况会让 LockSupport.park(this); 这句返回继续往下执行:
常规路径。signal -> 转移节点到阻塞队列 -> 获取了锁(unpark)
线程中断。在 park 的时候,另外一个线程对这个线程进行了中断
signal 的时候我们说过,转移以后的前驱节点取消了,或者对前驱节点的CAS操作失败了
假唤醒。这个也是存在的,和 Object.wait() 类似,都有这个问题
线程唤醒后第一步是调用 checkInterruptWhileWaiting(node) 这个方法,此方法用于判断是否在线程挂起
期间发生了中断,如果发生了中断,判断是 signal 调用之前中断的,还是 signal 之后发生的中断
*/
// 在节点入条件队列的时候,初始化时设置了 waitStatus = Node.CONDITION
// 前面我提到,signal 的时候需要将节点从条件队列移到阻塞队列,
// 这个方法就是判断 node 是否已经移动到阻塞队列了
final boolean isOnSyncQueue(Node node) {
// 移动过去的时候,node 的 waitStatus 会置为 0,这个之后在说 signal 方法的时候会说到
// 如果 waitStatus 还是 Node.CONDITION,也就是 -2,那肯定就是还在条件队列中
// 如果 node 的前驱 prev 指向还是 null,说明肯定没有在 阻塞队列(prev是阻塞队列链表中使用的)
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果 node 已经有后继节点 next 的时候,那肯定是在阻塞队列了
if (node.next != null)
return true;
// 下面这个方法从阻塞队列的队尾开始从后往前遍历找,如果找到相等的,
//说明在阻塞队列,否则就是不在阻塞队列
// 可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?答案是:不能。
// 这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
// 然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
return findNodeFromTail(node);
}
// 从阻塞队列的队尾往前遍历,如果找到,返回 true
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
//回到前面的循环,isOnSyncQueue(node) 返回 false 的话,那么进到 LockSupport.park(this); 这里线程挂起
// 检查条件等待时的线程中断情况
// 1. 如果在 signal 之前已经中断,返回 THROW_IE
// 2. 如果是 signal 之后中断,返回 REINTERRUPT
// 3. 没有发生中断,返回 0
private int checkInterruptWhileWaiting(Node node) {
// 中断请求在signal操作之前:THROW_IE
// 中断请求在signal操作之后:REINTERRUPT
// 期间没有收到任何中断请求:0
return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
//Thread.interrupted():如果当前线程已经处于中断状态,那么该方法返回 true,同时将中断状态重置为 false,所以,才有后续的 重新中断(REINTERRUPT) 的使用
// 将取消条件等待的结点从等待队列转移到同步队列中
// 只有线程处于中断状态,才会调用此方法
// 如果需要的话,将这个已经取消等待的节点转移到阻塞队列
// 返回 true:如果此线程在 signal 之前被取消
final boolean transferAfterCancelledWait(Node node) {
// 用 CAS 将节点状态设置为 0
// 如果这步 CAS 成功,说明是 signal 方法之前发生的中断,
// 因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 状态修改成功后就将该结点放入同步队列尾部
// 即将节点放入阻塞队列
// 这里我们看到,即使中断了,依然会转移到阻塞队列
enq(node);
return true;
}
// 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0
// signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成
// 当然,这种事情还是比较少的吧:signal 调用之后,没完成转移之前,发生了中断
// 到这里表明CAS操作失败, 说明中断发生在signal方法之后
while (!isOnSyncQueue(node)) {
// 如果sinal方法还没有将结点转移到同步队列, 就通过自旋等待一下
Thread.yield();
}
return false;
}
//这里再说一遍,即使发生了中断,节点依然会转移到阻塞队列
在以上两个操作完成了之后就会进入 while 循环,可以看到 while 循环里面首先调用 LockSupport.park(this)
将线程挂起了,所以线程就会一直在这里阻塞。在调用 signal 方法后仅仅只是将结点从等待队列转移到同步队列中去,至于会不会唤醒线程需要看情况。
如果转移节点时发现同步队列中的前驱节点已取消,或者是更新前驱节点的状态为 SIGNAL 失败,这两种情况都会立即唤醒线程,否则的话在 signal 方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前驱节点来唤醒。
当然,线程阻塞在这里除了可以调用 signal 方法唤醒之外,线程还可以响应中断,如果线程在这里收到中断请求就会继续往下执行。可以看到线程醒来后会马上检查是否是由于中断唤醒的还是通过 signal 方法唤醒的,如果是因为中断唤醒的同样会将这个节点转移到同步队列中去,只不过是通过调用 transferAfterCancelledWait()
方法来实现的。最后执行完这一步之后就会返回中断情况并跳出 while 循环。
到这里,大家应该都知道这个 while 循环怎么退出了吧。要么中断,要么转移成功
这里描绘了一个场景,本来有个线程,它是排在条件队列的后面的,但是因为它被中断了,那么它会被唤醒,然后它发现自己不是被 signal 的那个,所以它会自己主动去进入到阻塞队列
结点移出等待队列后的操作,即 while 循环出来以后,下面是这段代码::
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
/*
由于 while 出来后,我们确定节点已经进入了阻塞队列,准备获取锁。
这里的 acquireQueued(node, savedState) 的第一个参数 node 之前已经经过 enq(node) 进入了队列,
参数 savedState 是之前释放锁前的 state,这个方法返回的时候,代表当前线程获取了锁,
而且 state == savedState了。
注意,前面我们说过,不管有没有发生中断,都会进入到阻塞队列,而 acquireQueued(node, savedState)
的返回值就是代表线程是否被中断。如果返回 true,说明被中断了,而且 interruptMode != THROW_IE,
说明在 signal 之前就发生中断了,这里将 interruptMode 设置为 REINTERRUPT,用于待会重新中断
*/
// 这步操作主要为防止线程在signal之前中断而导致没与等待队列断绝联系
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
/*
这边说说 node.nextWaiter != null 怎么满足。我前面也说了 signal 的时候会将节点转移到阻塞队列,
有一步是 node.nextWaiter = null,将断开节点和条件队列的联系。
可是,在判断发生中断的情况下,是 signal 之前还是之后发生的? 这部分的时候,我也介绍了,
如果 signal 之前就中断了,也需要将节点进行转移到阻塞队列,这部分转移的时候,
是没有设置 node.nextWaiter = null 的。
之前我们说过,如果有节点取消,也会调用 unlinkCancelledWaiters 这个方法,就是这里了
*/
/*
到这里,我们终于可以好好说下这个 interruptMode 干嘛用了。
0:什么都不做,没有被中断过;
THROW_IE:await 方法抛出 InterruptedException 异常,因为它代表在 await() 期间发生了中断;
REINTERRUPT:重新中断当前线程,因为它代表 await() 期间没有被中断,
而是 signal() 以后发生的中断
*/
// 结束条件等待后根据中断情况做出相应处理
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 如果中断模式是THROW_IE就抛出异常
if (interruptMode == THROW_IE) {
throw new InterruptedException();
// 如果中断模式是REINTERRUPT就自己挂起
} else if (interruptMode == REINTERRUPT) {
selfInterrupt();
}
}
当线程终止了 while 循环也就是条件等待后,就会回到同步队列中。不管是因为调用 signal 方法回去的还是因为线程中断导致的,节点最终都会在同步队列中。这时就会调用 acquireQueued
方法执行在同步队列中获取锁的操作,这个方法我们在独占模式这一篇已经详细的讲过。
也就是说,节点从等待队列出来后又是乖乖的走独占模式下获取锁的那一套,等这个结点再次获得锁之后,就会调用 reportInterruptAfterWait
方法来根据这期间的中断情况做出相应的响应。如果中断发生在 signal 方法之前,interruptMode 就为 THROW_IE,再次获得锁后就抛出异常;如果中断发生在 signal 方法之后,interruptMode 就为 REINTERRUPT,再次获得锁后就重新中断
3. 不响应线程中断等待
// 不响应线程中断的条件等待
public final void awaitUninterruptibly() {
// 将当前线程添加到等待队列尾部
Node node = addConditionWaiter();
// 完全释放锁并返回当前同步状态
int savedState = fullyRelease(node);
boolean interrupted = false;
// 结点一直在while循环里进行条件等待
while (!isOnSyncQueue(node)) {
// 等待队列中所有的线程都在这里被挂起
LockSupport.park(this);
// 线程醒来发现中断并不会马上去响应
if (Thread.interrupted()) {
interrupted = true;
}
}
if (acquireQueued(node, savedState) || interrupted) {
// 在这里响应所有中断请求, 满足以下两个条件之一就会将自己挂起
// 1.线程在条件等待时收到中断请求
// 2.线程在acquireQueued方法里收到中断请求
selfInterrupt();
}
}
4. 设置相对时间不自旋等待
// 设置定时条件等待(相对时间), 不进行自旋等待
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
// 如果线程被中断则抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 将当前线程添加到等待队列尾部
Node node = addConditionWaiter();
// 在进入条件等待之前先完全释放锁
int savedState = fullyRelease(node);
long lastTime = System.nanoTime();
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 判断超时时间是否用完了
if (nanosTimeout <= 0L) {
// 如果已超时就需要执行取消条件等待操作
transferAfterCancelledWait(node);
break;
}
// 将当前线程挂起一段时间, 线程在这期间可能被唤醒, 也可能自己醒来
LockSupport.parkNanos(this, nanosTimeout);
// 线程醒来后先检查中断信息
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
long now = System.nanoTime();
// 超时时间每次减去条件等待的时间
nanosTimeout -= now - lastTime;
lastTime = now;
}
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
// 返回剩余时间
return nanosTimeout - (System.nanoTime() - lastTime);
}
5. 设置相对时间自旋等待
// 设置定时条件等待(相对时间), 进行自旋等待
public final boolean await(long time, TimeUnit unit) throws InterruptedException {
if (unit == null) { throw new NullPointerException(); }
// 获取超时时间的纳秒数
long nanosTimeout = unit.toNanos(time);
// 如果线程被中断则抛出异常
if (Thread.interrupted()) { throw new InterruptedException(); }
// 将当前线程添加等待队列尾部
Node node = addConditionWaiter();
// 在进入条件等待之前先完全释放锁
int savedState = fullyRelease(node);
//获取当前时间的毫秒数
long lastTime = System.nanoTime();
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 如果超时就需要执行取消条件等待操作
// 即时间到啦
if (nanosTimeout <= 0L) {
// 这里因为要 break 取消等待了。取消等待的话一定要调用
// transferAfterCancelledWait(node) 这个方法
// 如果这个方法返回 true,在这个方法内,将节点转移到阻塞队列成功
// 返回 false 的话,说明 signal 已经发生,signal 方法将节点转移了。也就是说没有超时嘛
timedout = transferAfterCancelledWait(node);
break;
}
// 如果超时时间大于自旋时间, 就将线程挂起一段时间
// spinForTimeoutThreshold 的值是 1000 纳秒,也就是 1 毫秒
// 也就是说,如果不到 1 毫秒了,那就不要选择 parkNanos 了,自旋的性能反而更好
if (nanosTimeout >= spinForTimeoutThreshold) {
LockSupport.parkNanos(this, nanosTimeout);
}
// 线程醒来后先检查中断信息
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
long now = System.nanoTime();
// 超时时间每次减去条件等待的时间
nanosTimeout -= now - lastTime;
lastTime = now;
}
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
// 返回是否超时标志
return !timedout;
}
6. 设置绝对时间等待
// 设置定时条件等待(绝对时间)
public final boolean awaitUntil(Date deadline) throws InterruptedException {
if (deadline == null) { throw new NullPointerException(); }
// 获取绝对时间的毫秒数
long abstime = deadline.getTime();
// 如果线程被中断则抛出异常
if(Thread.interrupted()) { throw new InterruptedException(); }
// 将当前线程添加到等待队列尾部
Node node = addConditionWaiter();
// 在进入条件等待之前先完全释放锁
int savedState = fullyRelease(node);
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 如果超时就需要执行取消条件等待操作
if (System.currentTimeMillis() > abstime) {
timedout = transferAfterCancelledWait(node);
break;
}
// 将线程挂起一段时间, 期间线程可能被唤醒, 也可能到了点自己醒来
LockSupport.parkUntil(this, abstime);
// 线程醒来后先检查中断信息
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 由于transferAfterCancelledWait方法没有把nextWaiter置空, 所有这里要再清理一遍
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
// 返回是否超时标志
return !timedout;
}
7. 唤醒等待队列中的头节点
调用 ConditionObject 的 signal() 方法,将会唤醒在等待时间最长的节点(即首节点),在唤醒首节点之前,会将节点移动到同步队列中
唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 唤醒正在等待的线程来消费。
// 唤醒等待了最久的线程
// 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列
public final void signal() {
// 调用 signal 方法的线程必须持有当前的独占锁
// 判断当前线程是否持有锁
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
Node first = firstWaiter;
// 如果等待队列中有排队者
if (first != null) {
// 唤醒等待 队列中的头结点
doSignal(first);
}
}
// 从条件队列队头往后遍历,找出第一个需要转移的 node
// 因为前面我们说过,有些线程会取消排队,但是可能还在队列中
//唤醒条件队列中的头结点
private void doSignal(Node first) {
do {
//1.将firstWaiter引用向后移动一位
// 将 firstWaiter 指向 first 节点后面的第一个,因为 first 节点马上要离开了
// 如果将 first 移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null
if ( (firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
//2.将头结点的后继结点引用置空
// 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉
first.nextWaiter = null;
//3.将头结点转移到同步队列, 转移完成后有可能唤醒线程
//4.如果transferForSignal操作失败就去唤醒下一个结点
// 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// 将指定节点从条件队列转移到阻塞队列
// true 代表成功转移
// false 代表在 signal 之前,节点已经取消了
final boolean transferForSignal(Node node) {
// CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,
// 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点
// 否则,将 waitStatus 置为 0
// 即将等待状态从CONDITION设置为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//如果更新状态的操作失败就直接返回false
//可能是transferAfterCancelledWait方法先将状态改变了, 导致这步CAS操作失败
return false;
}
// enq(node): 自旋进入阻塞队列的队尾
// 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点
//将该结点添加到同步队列尾部
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。
// 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,
// 上篇介绍的时候说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
// 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程
// 出现以下情况就会唤醒当前线程
// 1.前继结点是取消状态
// 2.更新前继结点的状态为SIGNAL操作失败
LockSupport.unpark(node.thread);
}
//方法返回 true,也就意味着 signal 方法结束了,节点进入了阻塞队列
return true;
}
可以看到 signal 方法最终的核心就是去调用 transferForSignal
方法,在 transferForSignal
方法中首先会用 CAS 操作将节点的状态从 CONDITION 设置为 0,然后再调用 enq
方法将该节点添加到同步队列尾部。
我们再看接下来的 if 判断语句,这个判断语句主要是用来判断什么时候会去唤醒线程,出现下面这两种情况就会立即唤醒线程:一种是当发现前继结点的状态是取消状态时,还有一种是更新前继结点的状态失败时。
这两种情况都会马上去唤醒线程,否则的话就仅仅只是将节点从条件队列中转移到同步队列中就完了,而不会立马去唤醒节点中的线程。
正常情况下,ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)
这句中,ws <= 0,而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL)
会返回 true,所以一般也不会进去 if 语句块中唤醒 node 对应的线程。if 语句判断结果为 false 时,这个方法返回 true,也就意味着 signal 方法结束了,节点进入了阻塞队列
signalAll 方法也大致类似,只不过它是去循环遍历条件队列中的所有节点,并将它们转移到同步队列,转移节点的方法也还是调用 transferForSignal
方法
8. 唤醒等待队列中的所有节点
// 唤醒等待队列后面的全部节点
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);
}