这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战
在学习多线程的时候,有一个很经典的问题,生产者消费者的问题。
一个消费者,一个生产者
-
生产者准备资源,当资源准备好后,通知消费者去消费资源
-
资源对于生产者和消费者来说,是互斥的,也就是说,同一时刻,只有一个线程可以获取到资源。
-
资源互斥这样做的好处可以防止资源的重复生产,重复消费或者资源还没有准备好就提前消费的情况。
以交替打印AB为例,
两个线程,要求线程1打印A,线程2打印B,最终的效果是两个线程交替打印。
解决这种问题的方法一般是设置一个boolean类型的变量flag和一把互斥锁,当flag的值为true时,线程1获取到互斥锁打印A,并修改变量flag为false,并唤醒线程B,并释放互斥锁,进行休眠,线程B获取到互斥锁后,打印B并修改flag为true,唤醒线程A,释放持有的互斥锁并休眠,以这样的形式,两个线程交替打印。
代码的具体实现是这样的。
class Resource {
private boolean flag = false;
public synchronized void printA() throws InterruptedException {
if (!flag) {
this.wait();
}
System.out.println("A");
this.flag = false;
this.notify();
}
public synchronized void printB() throws InterruptedException {
if (flag) {
this.wait();
}
System.out.println("B");
this.flag = true;
this.notify();
}
}
class Producer implements Runnable {
private final Resource resource;
Producer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
resource.printA();
} catch (InterruptedException e) {
break;
}
}
}
}
class Consumer implements Runnable {
private final Resource resource;
Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
this.resource.printB();
} catch (InterruptedException e) {
break;
}
}
}
}
public class Procon {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(new Consumer(resource)).start();
new Thread(new Producer(resource)).start();
}
}
多个生产者多个消费者
- 有一个int变量,有一类线程可以对该变量进行+1操作,有一类线程可以对该变量进行打印操作,现在要求,多个线程同时工作,并顺序打印整形的值。
class Resource {
private int i;
private boolean flag;
public synchronized void incr() throws InterruptedException {
while (flag) {
this.wait();
}
i++;
flag = true;
this.notifyAll();
}
public synchronized void print() throws InterruptedException {
while (!flag) {
this.wait();
}
System.out.println(i);
flag = false;
this.notifyAll();
}
}
public class MulitProCon {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(new Consumer(resource)).start();
new Thread(new Consumer(resource)).start();
new Thread(new Consumer(resource)).start();
new Thread(new Producer(resource)).start();
new Thread(new Producer(resource)).start();
new Thread(new Producer(resource)).start();
}
}
-
跟单线程的生产者的资源类中,变化不大,因为有了多个生产者和多个消费者,所以,在唤醒线程的时候,需要将原来的唤醒一个线程的
notify方法换成notifyAll唤醒所有线程的方法, -
因为唤醒所有的线程,所以,可能导致跟他同类的线程会被唤醒,比如说,线程执行了+1操作后,应该让输出线程输出i的值,因为我们将所有的线程都唤醒了,所以,可能有其他+1线程拿到锁并再次对变量进行+1,因为这里的原来的
if(flag)也要换成while(flag)以确保,跟他同类的线程被唤醒拿到锁后,也不能做相同的工作,只能释放自己的锁并休眠。 -
这种方式的缺点就是,虽然程序可以不报错执行,但是每次唤醒线程的时候,都需要唤醒所有的生产者线程和消费者线程,唤醒同类线程后,同类线程还需要继续被挂起,是没有意义的,而且,每次挂起线程和唤醒线程都需要通过从用户空间切换到内核空间,有很大的系统开销,所以,这并不是一个好的方法,我们针对这种缺点可以使用JDK提供的
ReentrantLock进行优化。
ReentrantLock优化多生产者多消费者
class Resource {
private int i;
private boolean flag;
private final ReentrantLock lock = new ReentrantLock();
private final Condition consumer = lock.newCondition();
private final Condition producer = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while (flag) {
producer.await();
}
i++;
flag = true;
consumer.signalAll();
} finally {
lock.unlock();
}
}
public void print() throws InterruptedException {
lock.lock();
try {
while (!flag) {
consumer.await();
}
System.out.println(i);
flag = false;
producer.signalAll();
} finally {
lock.unlock();
}
}
}
一个lock关联了两个Consumer队列,一个用于存储生产者线程,一个用户存储消费者线程,这样,在唤醒线程的时候就不会出现所有线程都被唤醒的情况,可以在一定的程度上降低系统的消耗。
生产者消费者的应用
java中很多很多工具或者中间件都是通过生产者消费者模式实现的,只要涉及到线程间通讯,或多或少都会使用到该涉及模式,他属于一种思想。具体是这样的,有三种角色,一种是生产者,一种是消费者,一种是资源,资源对于生产者和消费者来说是互斥的,生产者和消费者通过资源来进行线程间的协作通讯。
- 线程池
java中的线程池就是一个典型的生产者消费者模式,我们提交任务的行为就是生产者,如果任务不多时,直接通知
worker线程消费,此时worker线程就是消费者,如果任务过多,导致出现了积压,这时会把任务放到阻塞队列暂存,此时阻塞队列即充当了资源。
- Future接口
当我们提交任务给线程池通过
submit方法时,返回值是一个Future对象通过该对象的get方法即可获取到任务执行的结果,详情可以查看(Java中的Callable的返回值是怎么来的),如果任务没有执行完,那么调用该方法的线程会被挂起,直到任务执行完毕才会被唤醒。这种模式背后的原理其实也是生产者消费者。你品,你细品:-)