关于条件队列,你能说些什么?
条件队列是一个容器,它承载着一组等待“先验条件”成真的线程。
先验条件这个词文绉绉的,用白话讲就是你做一件事的前提条件。在代码里经常表现为你调用的方法能够执行的前提条件。举个例子,对于BlockingQueue你要调用put()方法,那么这个put方法能被成功调用的前提是这个blockingQueue不满。对于已满的情况,在同步的世界里,你可以抛异常、你可以返回一个特殊的自定义的值(在函数式编程里你可以做得更好)。在并发的世界里,如果能够block住并等到队列不满的时候再继续执行是更好的设计。“条件队列”就是用于这种“等待、通知、再运行”机制里的一个关键组件。
上面提到等待、通知时,你是不是马上想到了耳熟能详的sychronized关键字?因为在synchronized里,我们经常使用wati(), notify(), notifyAll()嘛。如果你直接想到object那也挺厉害的。
先验条件往往与对象的状态关联在一起,因为体现到代码上这些条件最终都是基于某些“对象属性”进行的布尔运算的结果,用这个结果来决定个先验条件是否成立。在并发的世界里,对这些对象属性必须同步才能避免多线程下因竞争而导致的问题,这就需要一个锁把这些先验条件用到的状态同步起来,所以就有了下面这样的逻辑:先验条件 --> 状态 --> 锁。为了检验一个条件,我们必须先持有锁。
回到上面blockingQueue的例子,我们先拿到这个队列的锁、再检查队列是否已满。如果队列已满,我们就不能继续执行put,需要block住,然后等候队列不满的通知。如何实现呢?调用wait()释放锁,等候条件成真后的通知notify,然后再继续执行。有可能得到条件变化为真的通知,但是拿到cpu时间槽要执行的时候,条件又false了,所以wait要写在while里,这算是个定式。
其实,讲到这里后面已经默默的使用到了condition queue。当你调用wait的时候,这个线程就进到了条件队列。而当有其它线程notify的时候,实际上就是通知条件队列里的线程(先验)条件发生了变化,让这些线程有机会重新去检查这些条件并继续运行。在这里,我们可以体会下调用wait()都隐式的对应到等待一个先验条件(条件谓词)为真,而调用notify/All都隐式的对应到一个先验条件发生了变化。
大家一般都知道sycnhronized与monitor(内置锁)紧密联系。进入同步代码块之前,你必须拥有monitor。实际上,条件队列也是与锁紧密联系的(甚至就是‘同一个’对象)。就内置条件队列来说,比较不好的一面是:调用wait()把线程放入这个内部条件队列意味着因为等待不同“先验条件”的线程都在同一队列中,就是说不同的先验条件共享同一个内部条件队列。这样在notifyAll进行唤醒的时候就并不高效了。
而Condition接口,可以帮助我们针对不同的先验条件创建不同的条件队列,这样就可以只唤醒与之对应的线程了。从锁与条件队列的关系你应该可以猜到,Lock接口提供了创建条件队列的方法。
public interface Lock {
Condition newCondition();
... ...
下面是Condition的接口定义,可以看到就如Lock是内部锁的泛化、显示化,而Condition就是内部条件队列的泛化、显示化。