花10分钟,你就能和别人分享Java中的Condition

123 阅读5分钟

Condition的使用

假设需要实现一个有界队列,在添加元素时,如果队列已满,则需要等待队列有空位时才能添加;若队列为空,则需要等待队列有元素时才能删除。

对于这种需求,仅使用Lock就会有点捉襟见肘了。为此,可选择使用Condition来解决这个问题。下面是相关的Demo:

public class BoundedQueue<T> {

    private Object[] items;
    private int addIndex, removeIndex, count;
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    public BoundedQueue(int size) {
        items = new Object[size];
    }

    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count >= items.length) {
                notFull.await();
            }

            items[addIndex] = t;
            addIndex++;
            if (addIndex == items.length) {
                addIndex = 0;
            }

            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }

            Object removeItem = items[removeIndex];
            removeIndex++;
            if (removeIndex == items.length) {
                removeIndex = 0;
            }
            count--;
            notFull.signal();
            return (T) removeItem;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        BoundedQueue<String> queue = new BoundedQueue<>(1);
        new Thread(() -> {
            try {
                System.out.println("remove await ");
                countDownLatch.await();
                for (;;) {
                    final String x = queue.remove();
                    System.out.println("remove item: " + x);
                    if (StringUtil.isNullOrEmpty(x)) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(2000);
        new Thread(() -> {
            try {
                countDownLatch.countDown();
                System.out.println("add item: hello");
                queue.add("hello");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                countDownLatch.countDown();
                System.out.println("add item: world");
                queue.add("world");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(1000);
        new Thread(() -> {
            try {
                System.out.println("add item: null");
                queue.add(null);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

用法和object.wait和object.notify是非常相似的。其区别在于,一个锁可对应多个condition,即可以有多个等待队列与锁对应。而使用object的wait和notify时,一个锁只能有一个等待队列与之对应。相比之下,condition的await和signal相对灵活许多。

那Condition要如何实现才能达到上述效果呢?

为方便叙述,假设现存一个AQS,其中的结构为: image.png

每个节点都是以独占模式的方式存在于AQS中。

等待队列的创建

要使用Condition,首先要创建一个等待队列。创建等待队列的方式很简单,只需要new出来即可

Condition condition = lock.newCondition();

对应地,在AQS中,就会创建了一个ConditionObject的对象。在这个ConditionObject中,有firstWaiter和lastWaiter,它们分别指向等待队列中头结点和尾结点。现在刚创建出来,它们现在的指向肯定是null。

image.png

在等待队列中等待

new出来之后如何使用呢?只需要调用await方法,即可让线程处于等待队列中。

condition.await();

如图所示,在调用await方法时,实际上是做了以下几件事

image.png

  1. 先将当前线程构造出一个新的Node,此处命名为 Node_Waiter1,然后进入等待队列。注意这里并没有复用原有的Node1;
  2. 调用release方法释放锁并唤醒同步队列的后继线程,这样就会让处于同步队列队头位置的Node1出队,并且Node2也会被唤醒;这么做的目的,是因为此时Node1所代表的线程需要等待某些条件成立才能继续进行,但是后继线程并不需要因为它的前继结点处于等待而继续被阻塞,而且此时锁也被释放了,所以此时后继结点的线程需要被唤醒。
  3. Node1出队后,由第1步构造的 Node_Waiter1 就会调用LockSupport.park(this)方法,让当前线程进入BLOKED状态。
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();

    // 对应第1步,构造新结点,加入等待队列
    Node node = addConditionWaiter();
    
    // 对应第2步,释放锁,唤醒同步队列中的后继结点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {

        // 对应第3步,让调用线程进入阻塞状态。注意这里的node变量并不是指Node1,而是出于等待队列中的Node_Waiter1。
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 一旦这个新的Node_Waiter1结点被唤醒,它就会加入到同步队列,通过acquireQueued方法和其他线程排队竞争同步锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

需要注意:

  • 当调用await方法,意味着该线程会释放锁,其他线程会获得锁。也就是说,Condition相关的方法,都要在成功获得锁的情况下调用。
  • 正因为Condition是在获得锁的情况下才能调用await,在独占模式下,获得锁的线程只有一个,所有操作都是单线程的,所以在waiter结点加入等待队列时,都没有采取保证线程安全的处理。

唤醒等待队列中的结点

处于等待队列中的结点,当条件成立时,可以通过signal方法被唤醒。

condition.signal();

当执行了signal方法后,原本在等待队列的结点会被移出等待队列,接着摇身一变,加入同步队列,成为其中一员,继续内卷。其过程大致如图所示:

image.png

signal相关的代码如下所示

public final void signal() {
    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);
}


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

核心方法是transferForSignal,在这个方法中,会将等待结点加入到同步队列中,并将状态从CONDITION改为SIGNAL。注意这里只是让node.thread进入同步队列,并没有去竞争同步锁,而是在唤醒线程,在await方法中调用acquireQueued方法后才去竞争同步锁。而acquireQueued方法,是在调用await方法的时候调用的。

总结时刻

你需要记住两大方面:

1. Condition的使用

和object.wait和notify类似,都需要将await和signal放到一个循环中进行调用,目的是为了防止线程不满足业务上的条件但被意外唤醒。

2. Condition的原理

  • 本质上还是使用单向链表构造的队列,每个Condition对象都是一个队列,需要在获取到锁之后才能使用Condition。
  • await表示原本变成新结点入队,已经持有的锁要释放,因为当前线程阻塞在了Condition上,不需要再阻塞其他线程获取锁,同时也因为其他线程需要获取到锁才能调用condition.signal()
  • signal表示等待队列中的头结点出队,接着加入到同步队列中,和其他线程竞争锁。