Java多线程(八)生产者消费者——Condition和精准唤醒
什么是Condition
对于任意一个java对象,它都拥有一组定义在java.lang.Object上监视器方法,包括wait(),wait(long timeout),notify(),notifyAll(),这些方法配合synchronized关键字一起使用可以实现等待/通知模式。同样,Condition接口也提供了类似Object监视器的方法,通过与Lock配合来实现等待/通知模式。可以看一下Object类的监视器方法和Condition接口的对比:
Condition解决生产者消费者问题
假设生产者可以生产票,但是现存的票只能有一张,只有顾客买走了才能再生产一张票,因此可以用Condition来保证同步。havenum表示有票,需要生产者等待;nonum表示没票,需要消费者等待。代码如下:
class tickets{
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition nonum = lock.newCondition();
Condition havenum = lock.newCondition();
public void put() throws InterruptedException {
lock.lock();
try{
while(num==1)
{
nonum.await();
}
num++;
System.out.println(Thread.currentThread().getName()+" 生产了一份,现存数量是 "+num);
havenum.signalAll();
}
finally {
lock.unlock();
}
}
public void take() throws InterruptedException {
lock.lock();
try {
while(num==0)
{
havenum.await();
}
num--;
System.out.println(Thread.currentThread().getName()+" 消耗了一份,现存数量是 "+num);
nonum.signalAll();
}
finally {
lock.unlock();
}
}
}
Condition 精准唤醒
上一篇文章Java多线程(七)生产者消费者——wait && notify && 虚假唤醒中说到可以用Condition机制进行按顺序唤醒,通过上面的例子我们也可以发现使用不同的Condition对象可以唤醒不同的线程,使用这一机制就可以做到精准唤醒。
class Aweaken
{
ReentrantLock lock = new ReentrantLock();
int num = 1;
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
public void weakA()
{
lock.lock();
try {
while(num != 1)
{
conditionA.await();
}
num = 2;
System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程B");
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void weakB()
{
lock.lock();
try {
while(num != 2)
{
conditionB.await();
}
num = 3;
System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程C");
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void weakC()
{
lock.lock();
try {
while(num != 3)
{
conditionC.await();
}
num = 1;
System.out.println("现在是线程 "+Thread.currentThread().getName()+", 下一个应该是线程A");
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class PCold {
public static void main(String[] args) {
Aweaken b = new Aweaken();
new Thread(()->{
for (int i = 0; i < 10; i++) {
b.weakA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
b.weakB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
b.weakC();
}
},"C").start();
}
}
Condition 实现分析
等待队列
ConditionObject的等待队列是一个FIFO队列,队列的每个节点都是等待在Condition对象上的线程的引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await(),那么该线程就会释放锁,构成节点加入等待队列并进入等待状态。
从下图可以看出来Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并更新尾节点即可。上述节点引用更新过程没有使用CAS机制,因为在调用await()的线程必定是获取了锁的线程,该过程由锁保证线程的安全。
一个Lock(同步器)拥有一个同步队列和多喝等待队列(如下图所示)
等待
调用Condition的await()方法,会使得当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()返回时,当前线程一定是获取了Condition相关联的锁。
线程触发await()这个过程可以看作是同步队列的首节点(当前线程肯定是成功获得了锁,因此一定是在同步队列的首节点)移动到了Condition的等待队列的尾节点,并释放同步状态进入等待状态,同时会唤醒同步队列的后继节点。
唤醒
调用Condition的signal()方法将会唤醒再等待队列中的首节点,该节点也是到目前为止等待时间最长的节点。调用Condition的signalAll()方法,将等待队列中的所有节点全部唤醒,相当于将等待队列中的每一个节点都执行一次signal()。