【JDK1.8源码阅读】ConditionObject相关源码实现分析整理(七)

Condition接口定义

Condition将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过await(),signal()来休眠/唤醒线程。

Object的监视器方法和Condition接口的特性对比如下:
image

源码定义如下:

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;

    // 通知其他和当前锁对象绑定的任一Condition
    void signal();

    // 通知所有和当前锁对象绑定的任一Condition
    void signalAll();
}

ConditionObject

ConditionObject实现了Condition接口,定义在AbstractQueuedSynchronizer中,每个ConditionObject包含一个等待队列,队列节点在AQS中已Node定义,条件队列的实现原理可以类比AQS同步队列的同步队列实现。
等待队列的基本结构如下所示:
image
Condition拥有首尾节点的引用,而新增节点(调用Condition的await()方法)只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

同步队列和条件队列的整体关系如下:
image

  1. 每次调用Condition.await方法,相当于将同步队列的首节点(获得锁的节点)移动到Condition的等待队列的尾部。
  2. 有节点调用Condition.signal方法时,则会将等待队列里的首节点唤醒移动到同步队列的尾节点中。

等待队列通过ConditionObject的两个成员变量操作:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;
}

堵塞等待实现

在ConditionObject,实现了Condition的多个方法来进行条件堵塞等待,包括:

  1. await()
  2. void awaitUninterruptibly()
  3. awaitNanos(long nanosTimeout)
  4. await(long time, TimeUnit unit)
  5. awaitUntil(Date deadline)。

这里主要看await()方法实现,其他实现大同小异

public final void await() throws InterruptedException {
    // 检测被打断,直接抛出
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程封装成Node加入到等待队列尾部
    Node node = addConditionWaiter();
    //释放锁
    int savedState = fullyRelease(node);
    // 记录wait结束后的中断行为,有3个可能值:
    // 0. 默认为0,表示不进行中断相关行为
    // 1. REINTERRUPT=1:重新interrupt()函数进行中断
    // 2. THROW_IE=-1:直接抛出中断异常
    int interruptMode = 0;
    //判断当前节点是否已经在同步队列中,如果是则退出循环,如果不是就阻塞当前线程
    //其他线程如果发出了signal信号之后,会把等待队列的线程移入同步队列,此时就会退出循环,进入下面的重新获取锁的acquireQueued
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //其他发出signal信号的线程释放锁之后,该线程被唤醒并重新竞争锁
    // acquireQueued是AQS的同步队列竞争锁逻辑实现,判断当前是否为节点,是则尝试获取锁,不是则堵塞直到成为首节点
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 取消node节点后续为取消状态的节点,这里等待状态不是CONDITION都被认为是取消状态
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

addConditionWaiter 插入等待队列

在实现开始尝试调用addConditionWaiter将当前线程封装成节点放进等待队列中,实现源码为:

private Node addConditionWaiter() {
    // 获取尾部即诶但
    Node t = lastWaiter;
    // 如果尾节点被取消,开始等待队列清理取消状态节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        // 从头开始遍历。去除状态异常节点
        unlinkCancelledWaiters();
        t = lastWaiter;//t指向最后一个状态正确的节点
    }
    // 生成一个新节点,状态为CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)//列表为空,初始化为第一个节点
        firstWaiter = node;
    else // 成为原尾节点的下一个节点
        t.nextWaiter = node;
    // 更新尾节点
    lastWaiter = node;
    return node;
}

// 遍历清理取消状态节点实现
private void unlinkCancelledWaiters() {
    // 先记为首节点,t代表遍历过程的每个节点
    Node t = firstWaiter;
    // trail为遍历到当前节点t前的的最后一个有效节点
    Node trail = null;
    while (t != null) { 
        // 缓存下一个节点,用于更新t
        Node next = t.nextWaiter;
        // 是否为取消状态
        if (t.waitStatus != Node.CONDITION) {
            // 消除引用关系
            t.nextWaiter = null;
            if (trail == null) // 所有节点无效
                // 更新第一个有效节点为firstWaiter
                firstWaiter = next;
            else
                // 记录有效节点的后置引用关联
                trail.nextWaiter = next;
            if (next == null) // 说明遍历结束,更新尾节点
                lastWaiter = trail;
        }
        else // 更新最后一个有效节点
            trail = t;
        // 更新t,进入下一轮遍历
        t = next;
    }
}

fullyRelease释放持有的锁

之前说在调用await方法前,condition对象必须取得锁,如果未取得锁,会抛出异常,这是在fullyRelease中实现的,下面看源码:

final long fullyRelease(Node node) {
    // 记录释放状态是否失败,默认为true
    boolean failed = true;
    try {
        long savedState = getState();
        // 尝试释放,释放成功返回true,否则返回false
        if (release(savedState)) {
            // 记录失败成功
            failed = false;
            return savedState;
        } else {
            // 释放锁失败,说明本来不持有锁,抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 失败则更新节点为取消状态,待从等待队列中移除
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

其中release是AQS的原生实现,这里不再分析。

isOnSyncQueue是否在同步队列中

在将当前线程插入等待队列和释放锁后,会调用isOnSyncQueue判断自己是否在同步队列中,如果在,说明被唤醒,否则继续进行堵塞,isOnSyncQueue实现如下:

final boolean isOnSyncQueue(Node node) {
    //如果当前线程为condition状态,说明在等待队列中正常等待
    // 或者prev为null,prev是在同步队列中才有效的参数,说明当前不是同步队列的节点
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) 
        // 如果该节点的next(不是nextWaiter,next指针在CLH队列中指向下一个节点)状态不为null,则该节点一定在CLH队列中
        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为false堵塞被唤醒后会检查线程是否因为中断被唤醒,这是因为LockSupport.park会响应线程中断,但LockSupport.park结束是否因为线程中断(还是唤醒),需要通过中断标识位判断,这在checkInterruptWhileWaiting中完成:

private int checkInterruptWhileWaiting(Node node) {
    // 线程是否被中断
    // 是则调用transferAfterCancelledWait判断中断处理行为
    // 返回true表示需要抛出异常,false表示重新打断
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}


final boolean transferAfterCancelledWait(Node node) {
    //cas将该节点状态由CONDITION变成0
    // 如果成功,调用enq将该节点从CONDITION队列添加到同步队列中(但是在CONDITION队列中的nextWaiter连接并没有取消)
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 更新状态成功,可以安心入队
        enq(node);
        // 这里表示后续要抛出异常
        return true;
    }
    
    // cas失败,则死循环判断直到进入同步队列,退出后走上面逻辑
    while (!isOnSyncQueue(node))
        Thread.yield();
    // 这里表示后续要调用中断函数进行重新中断
    return false;
}

后续在调用acquireQueued堵塞直到拿到锁或被中断,如果是被中断,且之前记录的中断模式为初始状态,则更新为重新中断。这个操作在reportInterruptAfterWait中完成:

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

堵塞唤醒

在Condition中,操作唤醒其他Condition对象的函数有signal()和signalAll(),两个方法的实现非常类似:

public final void signal() {
    // 如果当前线程没有持有锁,抛异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取等待队列首节点
    Node first = firstWaiter;
    if (first != null)
        // 唤醒首节点,移动到同步队列
        doSignal(first);
}


public final void signalAll() {
    // 如果当前线程没有持有锁,抛异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取等待队列首节点
    Node first = firstWaiter;
    if (first != null)
        // 唤醒所有节点,移动到同步队列
        doSignalAll(first);
}

从上面看到,两个函数的区别分别是doSignal和doSignalAll的调用,下面看看这两个函数的源码实现:

//对等待队列中从首部开始的第一个CONDITION状态的节点,执行transferForSignal操作,将node从等待队列中转换到同步队列队列中,同时修改同步队列中原先尾节点的状态
private void doSignal(Node first) {
    do {
		//当前循环将first节点从CONDITION队列transfer到同步队列
		//从CONDITION队列中删除first节点,调用transferForSignal将该节点添加到同步队列中,成功则跳出循环
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 将CONDITION队列中所有node出队,逐个添加到同步队列末尾,同时修改它们在同步队列中前驱节点的状态,修改为signal成功,则不用在此处唤醒该节点的线程,唤醒工作交给同步队列中的前驱节点,否则需要在此处park当前线程。
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
		//将first节点从CONDITION队列中出队
        Node next = first.nextWaiter;
        first.nextWaiter = null;
		//将first节点在同步队列中入队,同时可能需要执行unpark操作
        transferForSignal(first);
		//更新first的指向
        first = next;
    } while (first != null);
}

在两个方法中,都调用了transferForSignal,内部主要实现将一个节点从等待队列移动到同步队列的逻辑,具体代码如下:

// 1. 先尝试将等待队列首节点由CONDITION转换为初始状态,如果失败,返回转义失败,
// 2. 调用enq()将该node添加到同步队列中,成为新的尾节点,返回原来的尾节点
// 3. 若同步队列原先尾节点为CANCELLED或者对原先尾节点CAS设置成SIGNAL失败,则唤醒node节点;若cas失败,说明节点在同步队列总前驱节点已经是signal状态了,唤醒工作交给前驱节点(节省了一次park和unpark操作)
final boolean transferForSignal(Node node) {
    //如果CAS失败,则当前节点的状态为CANCELLED
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //enq将node添加到CLH队列队尾,返回node的prev节点p
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果p是一个取消了的节点,或者对p进行CAS设置失败,则唤醒node节点,让node所在线程进入到acquireQueue方法中,重新进行相关操作
    //否则,由于该节点的前驱节点已经是signal状态了,不用在此处唤醒await中的线程,唤醒工作留给CLH队列中前驱节点
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

下面展示调用signal函数将等待队列的首节点移动到同步队列的过程:
image

参考

  1. https://www.jianshu.com/p/f2f7cb4c48cd?from=timeline
  2. https://blog.csdn.net/u011470552/article/details/76571472
  3. Java并发编程的艺术
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值