概述
在之前的博客中介绍了wait()和notify()/notifAll(),它们只能选择随机唤醒或者全部唤醒等待锁的线程,如果遇到多个保护条件时容易导致过早唤醒问题,而Condition接口很好的解决了这个问题,它十分灵活,可以实现多路通知和选择性通知。文字有点抽象,看例子就明白了。
使用示例
场景:肯德基的货架中可以摆放各种食品,假设每个柜台只能卖一种食品,后厨可以生产各种食品放在货架上,当一种食品缺货时对应柜台则暂停出售,后厨补充了对应食品时会喊前台人员继续出售。
- 使用synchronized的写法:
public class Demo {
static final Object obj = new Object();
static int hamburger = 5;
static int chips = 0;
static class Cook extends Thread{
@Override
public void run() {
try {
while (true) {
if (hamburger == 0 ) {
synchronized (obj) {
hamburger++;
obj.notifyAll();
}
}
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class HamburgerCounter extends Thread{
@Override
public void run() {
try {
for (;;) {
while (hamburger > 0) {
System.out.println("HamburgerCounter卖出了第" + hamburger + "个汉堡");
hamburger--;
Thread.sleep(500);
}
synchronized (obj) {
System.out.println("汉堡已卖完,进入等待");
obj.wait();
System.out.println("收到通知,继续卖汉堡");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ChipsCounter extends Thread{
@Override
public void run() {
try {
for (;;) {
while (chips == 0) {
synchronized (obj) {
System.out.println("薯条已卖完,进入等待");
obj.wait();
System.out.println("收到通知,继续卖薯条");
}
}
chips--;
System.out.println("ChipsCounter卖出了第" + chips + "包薯条");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Cook().start();
new HamburgerCounter().start();
new ChipsCounter().start();
}
}
运行结果:
HamburgerCounter卖出了第5个汉堡
HamburgerCounter卖出了第4个汉堡
HamburgerCounter卖出了第3个汉堡
HamburgerCounter卖出了第2个汉堡
HamburgerCounter卖出了第1个汉堡
薯条已卖完,进入等待
汉堡已卖完,进入等待
收到通知,继续卖汉堡
收到通知,继续卖薯条
薯条已卖完,进入等待
HamburgerCounter卖出了第1个汉堡
汉堡已卖完,进入等待
收到通知,继续卖汉堡
HamburgerCounter卖出了第1个汉堡
收到通知,继续卖薯条
薯条已卖完,进入等待
汉堡已卖完,进入等待
由运行结果可知,在Cook中生产汉堡后,唤醒等待的柜台notifyAll时,唤醒了HamburgerCounter和ChipsCounter ,HamburgerCounter继续卖汉堡,ChipsCounter线程检测到Chips为0于是再次进入等待。这样做虽然完成了需求,但是唤醒了无需唤醒的线程,这样会触发多余的上下文切换影响性能。
- 使用condition写法
public class Demo {
static Lock lock = new ReentrantLock();
static Condition hamburgerCounter = lock.newCondition();
static Condition chipsCounter = lock.newCondition();
static int hamburger = 3;
static int chips = 0;
static class Cook extends Thread {
@Override
public void run() {
try {
while (true) {
if (hamburger == 0) {
try {
lock.lock();
hamburger++;
hamburgerCounter.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class HamburgerCounter extends Thread {
@Override
public void run() {
try {
for (; ; ) {
while (hamburger > 0) {
System.out.println("HamburgerCounter卖出了第" + hamburger + "个汉堡");
hamburger--;
Thread.sleep(500);
}
try {
lock.lock();
System.out.println( "汉堡已卖完,进入等待");
hamburgerCounter.await();
System.out.println("收到通知,继续卖汉堡");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ChipsCounter extends Thread {
@Override
public void run() {
try {
for (; ; ) {
while (chips > 0) {
System.out.println("ChipsCounter卖出了第" + chips + "份薯条");
chips--;
Thread.sleep(500);
}
try {
lock.lock();
System.out.println("薯条已卖完,进入等待");
chipsCounter.await();
System.out.println("收到通知,继续卖薯条");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Cook().start();
new HamburgerCounter().start();
new ChipsCounter().start();
}
}
运行结果:
HamburgerCounter卖出了第3个汉堡
薯条已卖完,进入等待
HamburgerCounter卖出了第2个汉堡
HamburgerCounter卖出了第1个汉堡
汉堡已卖完,进入等待
收到通知,继续卖汉堡
HamburgerCounter卖出了第1个汉堡
汉堡已卖完,进入等待
收到通知,继续卖汉堡
HamburgerCounter卖出了第1个汉堡
在这个案例中,使用condition,生产了汉堡就精准的唤醒卖汉堡的线程,不会将卖薯条的唤醒,避免了过早唤醒的问题。
核心函数分析
await()
- 使线程进入等待,和Object.wait()类似,必须在当前线程持有Lock时才能调用。
- 每个Condition都会维护一个
等待队列
,等待队列在AQS中以内部类实现(ConditionObject)。 - 调用await方法时,释放锁唤醒
同步队列
中的后继节点,将当前线程加入相应的condition对象的等待队列中并令它阻塞。 - 当此线程被唤醒时会被加入到同步队列中竞争锁,直到竞争到了锁,await方法才会返回。
signal()
- 唤醒此condition等待队列中的首节点(等待最久的节点),和Object.notify()类似,必须在当前线程持有Lock时才能调用。
- 将等待队列中的首节点移动到同步队列中并唤醒它。
signalAll()
- 唤醒此condition等待队列中的所有节点,和Object.notifyAll()类似,必须在当前线程持有Lock时才能调用。
- 将等待队列中的全部节点移动到同步队列中,并唤醒每个节点的线程。
总结
condition是对锁更加精准的控制,使用方法大致和Object中的唤醒和等待机制类似,但是比它拥有更高的灵活性,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
更详细的学习condition推荐这篇博客:https://www.cnblogs.com/sheeva/p/6484224.html