ReentrantLock之Condition

Java传家宝:微信公众号(Java传家宝)、Java传家宝-CSND

Condition

​ 条件队列,在ReentrantLock里面继承了AQS的内部Condition的实现类ConditionObject,之前在AQS之ReentrantLock中由于篇幅问题没有继续讲解。先看一下之前画的AQS结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,每个线程节点都记录了nextWaiter属性,在我们之前加锁解锁分析没有见到他,其实他是用于条件队列的,用于形成单向链表结构。我们先看一下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();
}

在看在AQS中的实现ConditionObject定义了那些属性:

public class ConditionObject implements Condition, java.io.Serializable { 
    private transient Node firstWaiter; // 条件队列队头
    private transient Node lastWaiter; // 条件队列队尾
}

可以看到,记录了队头队尾而已,至于Node还是之前的定义。可以看一下条件队列的结构:

条件队列

await & signal

​ 在条件队列中,比较重要的就是awaitsignal方法了,即等待和唤醒,我们先看await的源码:

// AQS
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);
}
addConditionWaiter

首先调用await方法之后,我们需要将当前线程加入到条件队列当中,通过addConditionWaiter,如下:

// AQS
private Node addConditionWaiter() {
    Node t = lastWaiter; // 拿到条件队列的尾节点
    // 如果尾节点不为null && waitStatus不是CONDITION
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters(); // 清理掉取消等待的条件队列节点
        t = lastWaiter; // 拿到最新的尾节点
    }
    // 将当前线程包装为条件队列节点 初始化waitStatus为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() {
    Node t = firstWaiter; // 拿到条件队列处理节点 从前往后遍历 从首节点开始
    Node trail = null; // 最终记录的是当前处理的节点的上一个节点,用于链表操作清理处理节点
    while (t != null) { // 处理节点不为空
        Node next = t.nextWaiter; // 处理节点的下一个节点
        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; // 将下一个节点作为处理节点 继续上述操作
    }
}

可以看到,条件队列的入队操作,首先是拿到队列的尾节点,判断是否为null&&当前尾节点是否取消了等待;是则通过unlinkCancelledWaiters方法将条件队列中的所有取消等待的节点从前往后遍历,全部清理掉。最后将当前线程包装为条件队列的节点,等待状态默认为CONDITION,然后入队即可。

fullyRelease

然后我们回到await方法,如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted()) // 检查中断状态
        throw new InterruptedException();
    Node node = addConditionWaiter(); // 将线程加入条件队列
    int savedState = fullyRelease(node); // 完全释放锁 并保存锁状态 即重入次数
//...
}

可以看到,将当前线程入队之后,会释放完全释放锁资源(包括重入),让别的线程抢占。看一下fullyRelease里面的逻辑:

final int fullyRelease(Node node) {
    boolean failed = true; // 失败标志默认为true
    try {
        int savedState = getState(); // 拿到锁状态
        if (release(savedState)) { // 释放锁
            failed = false; // 成功更新失败标志位false
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed) // 释放锁失败会将当前节点的waitStatus设置位取消获取锁
            node.waitStatus = Node.CANCELLED;
    }
}

释放锁的过程呢在AQS之ReentrantLock有说到,这里就不在说了。

isOnSyncQueue

然后我们回到await方法,如下:

// AQS
public final void await() throws InterruptedException {
    //...
    while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
        LockSupport.park(this); // 不在就挂起
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // ...
}

拿到锁状态且释放掉锁资源后,通过isOnSyncQueue判断当前线程节点是否在阻塞队列中,看一下里面的逻辑:

final boolean isOnSyncQueue(Node node) {
    // 等待状态为CONDITION || 前驱节点为空 返回false
    // 一般进入同步队列会更改状态为0
    if (node.waitStatus == Node.CONDITION || node.prev == null) 
        return false;
    // 后继节点不为空 说明已经完全进入同步队列了 返回true
    // 因为进入同步队列最后的操作就是设置后继节点
    if (node.next != null) 
        return true;
    // 后继节点为空 && 前驱节点不为空 && 等待状态不为CONDITION
    return findNodeFromTail(node);
}
// 后继节点为空 && 前驱节点不为空 && 等待状态不为CONDITION
// 说明是未完全入队,设置了前驱节点,CAS操作不清楚,后继节点还未设置
// 从后往前遍历同步队列 直到找到对应的节点
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node) // 找到了返回true
            return true;
        if (t == null) // 找不到返回false
            return false;
        t = t.prev;
    }
}

可以看到,这一部分就是判断当前线常节点是否已经进入同步队列了。具体判断过程与节点进入同步队列的过程有关,大概就是先通过等待状态和前驱节点判断,在通过后继节点判断,还不能确定就通过从后往前遍历查找。最终判断如果不在同步队列就挂起当前线程。

signal

​ 唤醒线程。通过刚刚await操作,如果是第一次调用,一般不在同步队列中,而是在条件队列当中,所以会将当前线程挂起。也就是停留在如下代码上:

public final void await() throws InterruptedException {
    //...
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); //  线程挂起
    }
    // ...
}

这个时候,只有等待其他线程唤醒才能够继续工作,也就是signal方法。看一下实现:

public final void signal() {
    if (!isHeldExclusively()) // 当前线程持有锁才能唤醒
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null) // 同步队列不为空执行 为空还唤醒啥?
        doSignal(first); // 从first开始唤醒
}
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null) // 记录first的后继节点(条件队列)
            lastWaiter = null; // 后继节点为空 设置尾节点为null
        // 从条件队列中移除first 
        // 因为要进入同步队列了或者first的等待状态不是CONDITION。顺便也清理了
        first.nextWaiter = null; 
    } while (!transferForSignal(first) && // 从前往后找到满足条件的node
             (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))// 替换状态为0
        return false; // 替换失败返回 说明当前节点已经不是CONDITION状态了 直接返回找下一个节点
    // CAS成功执行下面代码
    Node p = enq(node); // 将当前条件队列节点放入同步队列 返回的是前继节点
    int ws = p.waitStatus; 
    // 前继节点取消等待||CAS替换状态为-1成功
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); // 唤醒当前节点对应的线程
    return true;
}

可以看到当线程经过进入条件队列,在到被挂起,在到经过signal的操作之后,条件队列的线程节点就加入到同步队列的队尾了。

signal之后

这个时候我们在回到await方法,线程被唤醒后,又干了啥:

// AQS
public final void await() throws InterruptedException {
    // ...
    while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
        LockSupport.park(this); // 不在就挂起
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //...
}

上一步 signal 之后,我们的线程由条件队列转移到了同步队列,之后就准备获取锁了。只要重新获取到锁了以后,继续往下执行checkInterruptWhileWaiting,看看干了啥:

private static final int REINTERRUPT =  1; // 表示重新中断
private static final int THROW_IE    = -1; // 表示抛出中断异常
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ? // 是否中断?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}
// 
final boolean transferAfterCancelledWait(Node node) {
    // CAS替换当前线程节点的等待状态为0
    // 这块成功只能是signal之前发生中断了
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node); // 成功加入同步队列 这里可以发现 1中断会加入同步队列 2signal也会加入同步队列
        return true;
    }
	// 失败进入循环
    // 这里是指signal方法已经将状态改变了,但是还没有完成将其加入到同步队列中的操作就被中断了
    while (!isOnSyncQueue(node)) // 是否在同步队列中
        Thread.yield(); // 不在等待其完成
    return false;
}

可以看到,checkInterruptWhileWaiting这个方法就是判断中断状态的,可以分为三个状态:

  • 0: 代表整个过程中一直没有中断发生。
  • 1 THROW_IE: 表示退出await()方法时需要抛出nterruptedException,这种模式对应于中断发生在signal之前
  • -1 REINTERRUPT: 表示退出await()方法时只需要再自我中断一下,这种模式对应于中断发生在signal之后
退出while循环

现在我们在回到await里面,观察这个循环体:

// AQS
public final void await() throws InterruptedException {
    // ...
    while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
        LockSupport.park(this); // 不在就挂起
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //...
}

​ 经过刚刚的分析,要退出这个循环的话,要么当前线程节点已经在同步队列中了,要么当前线程处于中断状态。现在假设退出循环了,看一下后面的逻辑:

public final void await() throws InterruptedException {
//...
    // 获取锁 && 中断状态不为THROW_IE
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // 不为null说明是在中断被加入到同步队列的
        unlinkCancelledWaiters(); // 将条件队列中取消等待的清理掉
    if (interruptMode != 0) 
        reportInterruptAfterWait(interruptMode);
}
// 中断状态不为0
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException(); // 抛出异常
    else if (interruptMode == REINTERRUPT)
        selfInterrupt(); // 自我中断
}

​ 在这一过程中我们尤其要关注中断,如前面所说,中断和signal所起到的作用都是将线程从条件队列中移除,加入到同步队列中去争锁,所不同的是,signal方法被认为是正常唤醒线程,中断方法被认为是非正常唤醒线程,如果中断发生在signal之前,则我们在最终返回时,应当抛出InterruptedException;如果中断发生在signal之后,我们就认为线程本身已经被正常唤醒了,这个中断来的太晚了,我们直接忽略它,并在await 返回时再自我中断一下,这种做法相当于将中断推迟至await0 返回时再发生。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值