【搞定Java并发编程】第18篇:队列同步器AQS源码分析之Condition接口、等待队列

AQS系列文章:

1、队列同步器AQS源码分析之概要分析

2、队列同步器AQS源码分析之独占模式

3、队列同步器AQS源码分析之共享模式

4、队列同步器AQS源码分析之Condition接口、等待队列


通过前面三篇关于AQS文章的学习,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列等待队列

要学习条件队列,就必须先了解Condition接口。

1、Condition接口概述

每个Java对象都拥有一组监视器方法,主要包括:wait()、wait(long  timeout)、notify() 和 notifyAll()方法,这些方法与synchronized关键字配合,可以实现等待 / 通知模式。

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待 / 通知模式,但是这两者在使用方法和功能特性上还是有差别的。

Object的监视器方法与Condition接口的对比

Condition定义了 等待 / 通知 的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,即Condition对象是依赖Lock对象的。

Condition的使用方式比较简单,需要注意在调用方法前获取锁,案例代码如下:

package zju.com.lock;

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 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.signal();   // 唤醒一个等待在Condition上的线程
		} finally {
			lock.unlock();
		}
	}
}

一般会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

Condition的主要方法如下表所示:

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。

2.1、等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。这里的用的节点类型也是同步器的静态内部类Node。

这里举个例子,便于帮助理解等待队列:

我们拿公共厕所做比喻,同步队列是主要的排队区,如果公共厕所没开放,所有想要进入厕所的人都得在这里排队。而等待队列主要是为条件等待设置的,我们想象一下如果一个人通过排队终于成功获取锁进入了厕所,但在方便之前发现自己没带手纸,碰到这种情况虽然很无奈,但是它也必须接受这个事实,这时它只好乖乖的出去先准备好手纸(进入等待队列等待),当然在出去之前还得把锁给释放了好让其他人能够进来,在准备好了手纸(条件满足)之后它又得重新回到同步队列中去排队。

当然进入等待队列的人并不都是因为没带手纸,可能还有其他一些原因必须中断操作先去等待队列中去排队,所以等待队列可以有多个,按照不同的等待条件而设置不同的等待队列。等待队列是一条单向链表,Condition接口定义了等待队列中的所有操作,AbstractQueuedSynchronizer内部的ConditionObject类实现了Condition接口。

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。

等待队列的基本结构

Condition拥有尾结点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上诉节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包JUC中的Lock(更确切的说是同步器)拥有一个同步队列和多个等待队列。

同步队列与等待队列

2.2、响应线程中断等待

调用Condition的await()方法,会使当前线程进入等待队列并释放锁,同时线程状态转变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

如果从队列的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了同步状态 / 锁的节点)移动到Condition的等待队列中。

下面看下ConditionObject的await()方法的源码:

// 响应线程中断的条件等待
public final void await() throws InterruptedException {
    // 如果线程被中断则抛出异常
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
    // 将当前线程添加到条件队列尾部
    Node node = addConditionWaiter();
    // 在进入条件等待之前先完全释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 线程一直在while循环里进行条件等待
    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之后,就会新建一个节点将当前线程包装起来然后放入等待队列尾部。注意,这个过程只是将节点添加到同步队列尾部而没有挂起线程。

下面看下释放锁的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;
        }
    }
}

将当前线程包装成结点添加到等待队列尾部后,紧接着就调用fullyRelease()方法释放锁。注意,方法名为fullyRelease也就是这步操作会完全的释放锁,因为锁是可重入的,所以在进行条件等待前需要将锁全部释放了,不然的话别人就获取不了锁了。如果释放锁失败的话就会抛出一个运行时异常,如果成功释放了锁的话就返回之前的同步状态。

下面来看看进行条件等待的源码:

// 线程一直在while循环里进行条件等待
while (!isOnSyncQueue(node)) {
    // 进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:
    // 1.同步队列的前继结点已取消
    // 2.设置同步队列的前继结点的状态为SIGNAL失败
    // 3.前继结点释放锁后唤醒当前结点
    LockSupport.park(this);
    // 当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待, 此时需要将结点移出等待队列
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
        break;
    }
}

// 检查条件等待时的线程中断情况
private int checkInterruptWhileWaiting(Node node) {
    // 中断请求在signal操作之前:THROW_IE
    // 中断请求在signal操作之后:REINTERRUPT
    // 期间没有收到任何中断请求:0
    return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}

// 将取消条件等待的结点从等待队列转移到同步队列中
final boolean transferAfterCancelledWait(Node node) {
    // 如果这步CAS操作成功的话就表明中断发生在signal方法之前
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 状态修改成功后就将该结点放入同步队列尾部
        enq(node);
        return true;
    }
    // 到这里表明CAS操作失败, 说明中断发生在signal方法之后
    while (!isOnSyncQueue(node)) {
        // 如果sinal方法还没有将结点转移到同步队列, 就通过自旋等待一下
        Thread.yield();
    }
    return false;
}

在以上两个操作完成了之后就会进入while循环,可以看到while循环里面首先调用LockSupport.park(this)将线程挂起了,所以线程就会一直在这里阻塞。在调用signal方法后仅仅只是将结点从等待队列转移到同步队列中去,至于会不会唤醒线程需要看情况。

如果转移节点时发现同步队列中的前驱节点已取消,或者是更新前驱节点的状态为SIGNAL失败,这两种情况都会立即唤醒线程,否则的话在signal方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前驱节点来唤醒。

当然,线程阻塞在这里除了可以调用signal方法唤醒之外,线程还可以响应中断,如果线程在这里收到中断请求就会继续往下执行。可以看到线程醒来后会马上检查是否是由于中断唤醒的还是通过signal方法唤醒的,如果是因为中断唤醒的同样会将这个节点转移到同步队列中去,只不过是通过调用transferAfterCancelledWait方法来实现的。最后执行完这一步之后就会返回中断情况并跳出while循环。

结点移出等待队列后的操作:

// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
    interruptMode = REINTERRUPT;
}
// 这步操作主要为防止线程在signal之前中断而导致没与等待队列断绝联系
if (node.nextWaiter != null) {
    unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
    reportInterruptAfterWait(interruptMode);
}
 
// 结束条件等待后根据中断情况做出相应处理
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,再次获得锁后就重新中断。

2.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();
    }
}

2.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);
}

2.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) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        // 如果超时时间大于自旋时间, 就将线程挂起一段时间
        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;
}

2.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;
}

2.7、唤醒等待队列中的头节点

调用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 {
        //1.将firstWaiter引用向后移动一位
        if ( (firstWaiter = first.nextWaiter) == null) {
            lastWaiter = null;
        }
        //2.将头结点的后继结点引用置空
        first.nextWaiter = null;
        //3.将头结点转移到同步队列, 转移完成后有可能唤醒线程
        //4.如果transferForSignal操作失败就去唤醒下一个结点
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

//将指定结点从条件队列转移到同步队列中
final boolean transferForSignal(Node node) {
    //将等待状态从CONDITION设置为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        //如果更新状态的操作失败就直接返回false
        //可能是transferAfterCancelledWait方法先将状态改变了, 导致这步CAS操作失败
        return false;
    }
    //将该结点添加到同步队列尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
        //出现以下情况就会唤醒当前线程
        //1.前继结点是取消状态
        //2.更新前继结点的状态为SIGNAL操作失败
        LockSupport.unpark(node.thread);
    }
    return true;
}

可以看到signal方法最终的核心就是去调用transferForSignal方法,在transferForSignal方法中首先会用CAS操作将节点的状态从CONDITION设置为0,然后再调用enq方法将该节点添加到同步队列尾部。

我们再看接下来的 if 判断语句,这个判断语句主要是用来判断什么时候会去唤醒线程,出现下面这两种情况就会立即唤醒线程:一种是当发现前继结点的状态是取消状态时,还有一种是更新前继结点的状态失败时。

这两种情况都会马上去唤醒线程,否则的话就仅仅只是将节点从条件队列中转移到同步队列中就完了,而不会立马去唤醒节点中的线程。signalAll方法也大致类似,只不过它是去循环遍历条件队列中的所有节点,并将它们转移到同步队列,转移节点的方法也还是调用transferForSignal方法。

节点从等待队列移动到同步队列

2.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);
}

AQS系列文章:

1、队列同步器AQS源码分析之概要分析

2、队列同步器AQS源码分析之独占模式

3、队列同步器AQS源码分析之共享模式

4、队列同步器AQS源码分析之Condition接口、等待队列

参考及推荐:

1、AbstractQueuedSynchronizer源码分析之概要分析

2、AbstractQueuedSynchronizer源码分析之独占模式

3、AbstractQueuedSynchronizer源码分析之共享模式

4、AbstractQueuedSynchronizer源码分析之条件队列

猜你喜欢

转载自blog.csdn.net/pcwl1206/article/details/84984713