Condition 源码分析

248 阅读5分钟

概述

我们知道synchronized通过使用Object.wait、Object.notify方法,可以实现条件等待、唤醒操作。
Lock怎么实现这些呢?就是通过Condition接口实现的。Condition接口的实现类ConditionObject是AbstractQueuedSynchronizer的内部类,AQS类是前两篇就分析的,但ConditionObject没有分析,说明欠的债迟早是要还的😂

先看demo的使用

private static void testUseCondition() throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread t0 = new Thread(() -> {
            lock.lock();
            System.out.println(currentThread().getName() + " 线程加锁运行");
            try {
                System.out.println(currentThread().getName() + " 线程进入等待,释放锁资源");
                condition.await();
                System.out.println(currentThread().getName() + " 线程被唤醒,继续运行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });
        t0.start();

        TimeUnit.SECONDS.sleep(2L);

        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println(currentThread().getName() + " 线程加锁运行");
            System.out.println(currentThread().getName() + " 线程唤醒condition等待");
            condition.signal();
            try {
                TimeUnit.SECONDS.sleep(1L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(currentThread().getName() + " 运行完成");
            lock.unlock();
        });
        t1.start();
    }

运行结果:

Thread-0 线程加锁运行
Thread-0 线程进入等待,释放锁资源
Thread-1 线程加锁运行
Thread-1 线程唤醒condition等待
Thread-1 运行完成
Thread-0 线程被唤醒,继续运行

代码依然可以在github中下载
简单分析结果:
t0线程调用condition.await后,释放锁资源进入等待状态。
t1线程做了唤醒操作,但锁没有释放,t0继续等待
当t1线程释放资源后,t0线程继续加锁运行。\

下面我们一起分析下ConditionObject源码:

ConditionObject的数据结构

public class ConditionObject implements Condition, java.io.Serializable {
    private transient Node firstWaiter;
    
    private transient Node lastWaiter;
    ...
}

上图清楚的展示了同步队列和条件等待队列的数据结构。最上面一行是同步队列,具体分析已经在AbstractQueuedSynchronizer源码分析中说明了,下面是condition队列,每个condition有一个单向列表等待队列(不然呢,毕竟数据结构是属于Condtion对象的,每个condition又是new出来的)

await方法

上图可以看到头节点从同步队列进入condition等待队列。

public final void await() throws InterruptedException {
    //响应中断的等待
    if (Thread.interrupted())
        throw new InterruptedException();
    //创建等待节点,加入等待队列    
    Node node = addConditionWaiter();
    //释放锁,并唤醒同步队列中的后继节点,将state值返回,暂存
    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);
}
  1. 判断是否被中断,是中断抛出InterruptedException
  2. 创建等待节点,加入到等待队列
  3. 释放锁,锁了多少次,全释放掉,唤醒后继节点,并暂存state值(因为锁是可重入的,每次重入state都会加一次和)
  4. 如果不在同步队列中,进行park,挂起自己。(要提醒自己进行LockSupport.park(this)后,代码就不再往下执行了
  5. 从挂起中被唤醒后(挂起被唤醒有两种情况:LockSupport.unpark即其他线程调用了signal方法或者其他线程调用中断方法),判断中断状态,为0跳出循环,进入下一步,不为0,说明是signal唤醒的,再次判断等待节点是否在同步队列中了。
  6. 成功唤醒后,节点进行请求队列操作(acquireQueued)
  7. 如果有后继等待节点,将取消状态的等待节点移除
  8. 根据中断模式的值进行中断或者抛出InterruptedException异常处理

addConditionWaiter方法

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
  1. 移除取消等待的节点
  2. 创建一个等待节点
  3. 如果等待队列为空,将该节点设为等待队列的头节点和尾节点
  4. 如果等待不为空,将该节点设为尾节点,并将之前的尾节点的nextWaiter指向该节点,如图5.11所示

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;
    }
}
  1. 得到该线程锁定的state值,暂存起来供唤醒以后恢复
  2. 释放锁资源
  3. 如果异常退出将node的waitStatus设置为取消状态

isOnSyncQueue方法

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        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;
    }
}
  1. 判断是否在等待队列中,在就返回false
  2. 如果node.prev!=null && node.next==null 肯定在同步队列中,返回true
  3. 否则在同步队列中找这个节点,找到就是true否则返回false

当进入同步等待挂起后,就等着别人唤醒了,下面我们一起看一下signal方法

signal方法

public final void signal() {
    //没有占用锁则抛出IllegalMonitorStateException异常
    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);
}
  1. 将头节点设为后继节点,如果没后继节点了,将尾节点也设为null
  2. 将节点移到同步队列中(当节点状态被设置取消是,transferForSignal方法返回false)
  3. 如果first节点被取消,将新头节点进行唤醒
final boolean transferForSignal(Node node) {
    //如果CAS更新失败,说明Node节点已经不是CONDITION状态,说明已被取消
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //加入同步队列 并返回同步队列中的前继节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果前继节点被取消或者CAS设置state为SIGNAL时失败了
    //唤起当前节点的线程,让它重新同步
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

我们signal和await串起来看

  1. 当await方法执行完这个LockSupport.park(this),就挂起了
  2. 当调用signal方法,执行enq(node)后,节点重新进入同步队列,并返回前继节点
  3. 如果前继节点是取消状态,或者前继节点的状态设置不成功,将节点唤起,唤起后后进入acquireQueued(node, savedState)方法
    3.1. 这个方法会再次尝试加锁,不成功会再次让自己挂起😂
  4. 当然如果ws<=0 && compareAndSetWaitStatus(p, ws, Node.SIGNAL)设置成功,那线程就安心休息就好了,因为前继节点已经设置了SIGNAL状态,它释放锁以后会叫醒你的