Condition-线程同步

166 阅读9分钟

Condition接口是处于java.util.concurrent.locks下的接口,提供了线程同步的一系列方法。在平时开发中,我们都是先创建一个ReentrantLock对象,然后获取到Condition的具体实现类来对线程进行同步。Condition有什么到底是怎么样使线程同步的呢?

当我们编写一个消费者生产者的demo时,代码模板可以这样写

pubilc class Test {
    
    private Object[] arr;
    private int size;
    private ReentrantLock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
 
    public Test(int length) {
        arr = new Object[length];
    }
     
    public void put(Object obj) throws Exception{
        lock.lock();
        try{
            while(size == object.length) {
                notFull.await();
            }
            arr[size] = obj;
            notEmpty.singal();
        } finally {
            lock.unlock();
        }
    }
    
    public Object get() throws Exception{
        lock.lock();
        try {
            while(size == 0) {
                notEmpty.await();
            }
            Object obj = arr[size--];
            notFull.singal();
            return obj;
        } finally {
            lock.unlock();
        }
    }
    
}

await方法时线程阻塞,singal方法唤醒线程,让我来探究探究其源码!

当我们使用ReentrantLock的newCondition方法时,返回的是一个ConditionObject对象,这个对象是被声明为AbstractQueuedSynchronizer类的内部类的实例!ConditionObject类维护了一个单向队列,用来记录被await方法阻塞的线程,节点还是使用的AbstractQueuedSynchronizer类的Node内部类

static final class Node {
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
}

当节点的状态为CONDITION时,代表此时线程被记录在ConditionObject类维护的队列中,其他状态在共享锁下的AQS中详细介绍过,这里就不赘述了。

在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;
}

image.png

await方法

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

await方法首先会判断当前线程是否被调用过interrupt方法,如果调用就抛出异常,可见await是响应线程中断的。然后调用addConditionWaiter方法将线程记录在ConditionObject类中的队列中,让我们先看看这个方法的实现

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

先获取到尾结点,然后遍历判断尾结点的状态是否不是CONDITION(节点的初始状态是CONDITION),如果节点的状态不是CONDITION,此时节点要么就在AbstractQueuedSynchronizer类的双向队列中,处于SIGNAL状态,要么就是已经被取消,这两种状态都需要从ConditionObject类的队列去除!进入到unlinkCanceledWaiters方法

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

这个方法其实就是从头结点开始,判断当前节点的状态是否不是Condition,如果是就断掉对当前节点的指针。trail.nextWaiter = next,trail代表当前节点的前一个节点。firstWaiter = next;这种情况是当前节点就是头结点!代码比较简单,就不赘述了

回到addConditionWaiter方法中,去除掉队列中的无用节点后,就新建一个节点,状态为CONDITION,然后加入到队列中,最后返回这个节点!

回到await方法中

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

在队列中记录完当前线程后,就需要释放当前线程获取的锁,这里释放了全部的同步信号!

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

获取到锁的全部state,调用release方法

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

release方法返回false只有tryRelease(arg)没有成功,在ReentrantLock中不成功有两种情况,一种是释放锁的线程和获取锁的线程不一致,此时会抛出异常;另外一种情况是释放的信号量之后,state还是不为0,也就是没有完全释放锁,就会返回false!所以fullyRelease在判断release返回false后就会抛出一个IllegalMonitorStateException异常,因为信号量不一致,虽然我不知道什么时候会出现这种情况!另外一种情况是直接抛出异常,然后进入finally代码块中,设置当前的节点状态为取消。如果成功,返回信号量!

回到await方法

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

进入到while循环,isOnSyncQueue这个方法见名知意,就是判断当前线程是否在AbstractQueuedSynchronizer类中的双向同步队列中,如果不存在使当前线程park!那么为什么线程在AbstractQueuedSynchronizer类中的双向同步队列中呢?AbstractQueuedSynchronizer类中的双向同步队列中和ConditionObject类中的单向队列有什么联系呢?

这里需要梳理一下思路,当我们调用await时,线程是处于ReentrantLock的同步代码块中的,如果线程被其他线程signal,就会被唤醒,但是线程在await时已经调用fullRelease方法释放了锁,并且此时线程处于同步代码块中,当线程执行完同步代码块中的代码后,进入到finally中释放锁就会抛出异常IllegalMonitorStateException!因为线程并没有获取锁!所以在signal时,我们不应该把线程直接唤醒,而是将线程加入到AbstractQueuedSynchronizer类中的双向同步队列中让其自己去抢占锁,这样才不会发送上方的问题。

await方法中使用while循环判断线程是否在双向同步队列中的意图就是相当于判断是否有其他线程调用signal方法!因为只有signal方法才会将单向队列中的节点加入到双向队列中!那我们就先看看signal方法的源码

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

signal方法也是在同步代码块中的,所以需要判断获取锁的线程是否是当前线程。然后把就会调用doSignal方法来将first节点加入到双向同步队列中!

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

此方法先将firstWaiter指针后移(删除当前首节点),然后判断是否只有first这一个节点,如果是就将lastWaiter也置空。然后断掉first节点的next指针,进入到transferForSignal方法

final boolean transferForSignal(Node node) {

    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

先设置node节点的状态从CONDITION变为0,如果节点是CANCELED状态肯定失败,就会回到do while循环唤醒下一个节点。如果成功就将当前节点加入到AbstractQueuedSynchronizer的双向同步队列中,需要注意的是enq方法返回的是node节点的前一个节点!最后判断下前一个节点是否是CANELED状态,如果是就直接唤醒当前节点记录的线程,如果不是就设置前一个节点的状态为SIGNAL,相当于为自己设置一个闹钟!最后返回true

所以被await方法的阻塞的线程要么就是在signal方法中被唤醒的,要么就是被双向同步队列中的前一个节点释放锁时唤醒的。总之线程被正常唤醒后肯定是处于双向同步队列中,然后退出while循环。

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

if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)这个if判断是什么意思呢?这是因为await方法阻塞的线程不仅可以使用signal方法唤醒,还可以使用interrupt方法唤醒,但是interrupt方法属于非正常唤醒(interrupt方法可以唤醒park方法阻塞的线程)。让我们进入到checkInterruptWhileWaiting方法

private static final int REINTERRUPT =  1;
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE    = -1;

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

THROW_IE状态代表线程被中断的时候还没有别的线程调用signal方法,REINTERRUPT状态代表线程被中断的时候已经有别的线程调用了signal方法

checkInterruptWhileWaiting首先判断线程是否被中断,如果被中断,进入到transferAfterCancelledWait方法

final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    /*
     * If we lost out to a signal(), then we can't proceed
     * until it finishes its enq().  Cancelling during an
     * incomplete transfer is both rare and transient, so just
     * spin.
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}
  1. compareAndSetWaitStatus(node, Node.CONDITION, 0)将node的状态由CONDITION状态设置为0,如果设置成功,证明此时线程还没有被signal过,因为在signal方法中,会将node节点的状态设置为0。将线程加入到双向同步队列中,返回true
  2. 如果线程已经被signal过了,那么此时线程可能还在进行加入到双向同步队列中的代码,所以进行了while循环判断,直到线程被记录在双向队列中,返回false
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);
}

现在我们知道while循环中的if判断如果返回结果是0,就代表线程没有被中断过。如果被中断过就不会返回0,直接跳出循环。无论如何,此时线程已经进入到了双向同步队列中!

acquireQueued方法中线程去抢占锁,直到抢占成功。在抢占锁的过程中,如果线程被中断过就会返回true,这里接着判断interruptMode != THROW_IE,就是判断线程是不是被signal之前就interrupt了,如果不是的话,就代表线程是在signal之后被中断的,设置interruptMode = REINTERRUPT。

需要注意的一点是线程被中断后在checkInterruptWhileWaiting中我们并没有清理加入到双向队列的节点,所以这里还需要进行清理

    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();

最后判断interruptMode是否发生过interrupt

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

这个方法的逻辑很简单,就是如果线程是在signal之后被interrupt的,就抛出异常,如果是在之前,设置线程的撞断标记。

如上,就是ConditionObject的大体逻辑