生产者消费者实例
1. 等待/通知机制
等待/通知机制在我们生活中很常见,例如,餐厅服务员和厨师之间,只有厨师做好菜之后,通知服务员,服务员才能上菜;而在未做好菜之前,服务员只能等待厨师做菜。除了这个例子外,等待/通知机制中,最典型的就是生产者和消费者模型,下边我们用代码实现该模型。
2. 单一生产者和消费者
Java中等待/通知,通常使用Object
类中的wait()
方法阻塞线程,线程进入等待吃,notify()
/notifyAll()
方法唤醒线程。其中notify()
和notifyAll()
方法的区别在于,notify()
方法,值唤醒等待池线程中的一个线程,而notifyAll()
唤醒所有等待池中的线程。下边我们使用代码,实现等待/通知中的典型例子,生产者和消费者模型。
以生产汽车为例,创建一个汽车工厂类,其中有两个一个生产汽车和一个销售汽车的方法:
@Slf4j
public class CarFactory {
private int num;
private Object obj;
public CarFactory(Object obj) {
this.obj = obj;
}
public void createCar() {
synchronized (obj) {
try {
while (num == 10) {
log.info("当前数量={},暂停生产", num);
obj.wait();
}
num++;
log.info("生产者:{}, 生产了一辆汽车,当前总量:{}", Thread.currentThread().getName(), num);
obj.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void consumerCar() {
synchronized (obj) {
try {
while (num == 1) {
log.info("当前数量={},暂停销售", num);
obj.wait();
}
num--;
log.info("消费者:{}, 购买了了一辆汽车,当前总量:{}", Thread.currentThread().getName(), num);
obj.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
创建两个线程,分别指代生产者和消费者:
@AllArgsConstructor
public class Producer implements Runnable {
private CarFactory carFactory;
@Override
public void run() {
while (true) {
carFactory.createCar();
}
}
}
@AllArgsConstructor
public class Consumer implements Runnable {
private CarFactory carFactory;
@Override
public void run() {
while (true) {
carFactory.consumerCar();
}
}
}
测试方法:
public static void main(String[] args) throws Exception {
CarFactory carFactory = new CarFactory(new Object());
Producer producer = new Producer(carFactory);
Consumer consumer = new Consumer(carFactory);
new Thread(producer, "生产者A").start();
new Thread(consumer, "消费者A").start();
}
上边创建了一个生产者和一个消费者,程序运行的过程大致如下:
- 生产者获得锁之后,开始生产产品;
- 生产者生产的数量达到10之后,调用
wait()
方法,阻塞生产者线程,生产者释放锁; - 生产者释放锁之后,消费者获得锁,消费一个后,调用
notify()
方法,唤醒一个等待池中的线程,而此时等待池中只有一个生产者,因此唤醒生产者,等待消费者释放锁; - 消费者消费的只剩一个之后,调用
wait()
方法,阻塞消费者,释放锁; - 生产者再次得到锁,然后再次从第1步开始执行。
需要注意的地方:判断线程是否该阻塞时,需要使用while
而不是if
;因为,线程唤醒之后,会继续从上一次停止的地方开始执行;如果使用if
,下一次唤醒之后,不会再次判断,会继续执行if
后边的代码,这里就是会继续生产产品,造成误差,单个生产者消费者时,可能不明显,多个生产者消费者时会更加明显的体现。
以上测试代码,执行结果:
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:2
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:3
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:4
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:5
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:6
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:7
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:8
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:9
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生产者:生产者A, 生产了一辆汽车,当前总量:10
19:24:33.673 [生产者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 当前数量=10,暂停生产
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:9
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:8
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:7
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:6
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:5
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:4
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:3
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:2
19:24:33.673 [消费者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消费者:消费者A, 购买了了一辆汽车,当前总量:1
.........
3. 多个生产者和消费者
上边展示了单个生产者消费者的情况,下边看一下多个生产者消费者的情况;之前我们说过notify()
方法,只会唤醒等待池中的一个线程,在多个生产者消费者的情况下,如果继续使用notify()
方法,每次就只能唤醒一个线程;
更加严重的情况可能会出现“假死”,即,消费者唤醒了一个生产者,然后释放锁,生产者A开始生产,同时,调用notify()
方法唤醒线程,如果唤醒了另一个生产者B;那么,当达到阻塞条件,生产者A进入WAITING
状态,生产者B得到锁,开始执行同步块,但是刚执行就发现达到了阻塞条件也进入WAITING
状态;此时,还没来得及唤醒线程;所有的线程都处于WAITING
状态,就是线程“假死”。
因此,多个生产者消费者我们需要使用notifyAll()
方法,唤醒所有的线程,其他地方可以不做修改;测试方法,多创建几个生产者和消费者就可以了,这里不再重复代码。
但是,使用notifyAll()
方法,唤醒了所有的等待池中的线程,但是,生产者生产过程中,如果即将达到阻塞状态时,唤醒所有线程,唤醒的生产者还是会再次阻塞,浪费时间;如果我们只唤醒消费者就了,使用Lock
和Condition
可以实现我们的需求:
@Slf4j
public class CarFactory {
private int num;
private Lock lock = new ReentrantLock();
private Condition producerCondition = lock.newCondition();
private Condition consumerCondition = lock.newCondition();
public void producerCar() {
try {
lock.lock();
while (num == 10) {
log.info("数量已达上限,停止生产");
producerCondition.await();
}
num++;
log.info("生产者:{}, 生产了一个汽车,总量为:{}", Thread.currentThread().getName(), num);
consumerCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumerCar() {
try {
lock.lock();
while (num == 1) {
log.info("没车了,停止销售");
consumerCondition.await();
}
num--;
log.info("消费者:{}, 购买了一个汽车,总量为:{}", Thread.currentThread().getName(), num);
producerCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
上边代码,创建了两个Condition对象,分别用来阻塞和唤醒生产者消费者;生产方法中,使用producerCondition
对象,阻塞生产者,但是使用consumerCondition
对象的singAll()
方法,来唤醒线程,因为消费者使用了也是该对象进行阻塞,因此,此时唤醒的都是消费者;对于消费者也是一样。
测试方法:
public static void main(String[] args) {
CarFactory carFactory = new CarFactory();
ExecutorService service = Executors.newCachedThreadPool();
Producer producer = new Producer(carFactory);
Consumer consumer = new Consumer(carFactory);
for (int i = 0; i < 3; i++) {
service.execute(producer);
service.execute(consumer);
}
service.shutdown();
}