线程间通信-生产者消费者模式

634 阅读5分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

在学习多线程的时候,有一个很经典的问题,生产者消费者的问题。

一个消费者,一个生产者

image.png

  1. 生产者准备资源,当资源准备好后,通知消费者去消费资源

  2. 资源对于生产者和消费者来说,是互斥的,也就是说,同一时刻,只有一个线程可以获取到资源。

  3. 资源互斥这样做的好处可以防止资源的重复生产,重复消费或者资源还没有准备好就提前消费的情况。

以交替打印AB为例,

两个线程,要求线程1打印A,线程2打印B,最终的效果是两个线程交替打印。 解决这种问题的方法一般是设置一个boolean类型的变量flag和一把互斥锁,当flag的值为true时,线程1获取到互斥锁打印A,并修改变量flagfalse,并唤醒线程B,并释放互斥锁,进行休眠,线程B获取到互斥锁后,打印B并修改flagtrue,唤醒线程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();
    }
}

多个生产者多个消费者

image.png

  1. 有一个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();
    }
}
  1. 跟单线程的生产者的资源类中,变化不大,因为有了多个生产者和多个消费者,所以,在唤醒线程的时候,需要将原来的唤醒一个线程的notify方法换成notifyAll唤醒所有线程的方法,

  2. 因为唤醒所有的线程,所以,可能导致跟他同类的线程会被唤醒,比如说,线程执行了+1操作后,应该让输出线程输出i的值,因为我们将所有的线程都唤醒了,所以,可能有其他+1线程拿到锁并再次对变量进行+1,因为这里的原来的if(flag)也要换成while(flag)以确保,跟他同类的线程被唤醒拿到锁后,也不能做相同的工作,只能释放自己的锁并休眠。

  3. 这种方式的缺点就是,虽然程序可以不报错执行,但是每次唤醒线程的时候,都需要唤醒所有的生产者线程和消费者线程,唤醒同类线程后,同类线程还需要继续被挂起,是没有意义的,而且,每次挂起线程和唤醒线程都需要通过从用户空间切换到内核空间,有很大的系统开销,所以,这并不是一个好的方法,我们针对这种缺点可以使用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中很多很多工具或者中间件都是通过生产者消费者模式实现的,只要涉及到线程间通讯,或多或少都会使用到该涉及模式,他属于一种思想。具体是这样的,有三种角色,一种是生产者,一种是消费者,一种是资源,资源对于生产者和消费者来说是互斥的,生产者和消费者通过资源来进行线程间的协作通讯。

  1. 线程池 java中的线程池就是一个典型的生产者消费者模式,我们提交任务的行为就是生产者,如果任务不多时,直接通知worker线程消费,此时worker线程就是消费者,如果任务过多,导致出现了积压,这时会把任务放到阻塞队列暂存,此时阻塞队列即充当了资源。

image.png

  1. Future接口 当我们提交任务给线程池通过submit方法时,返回值是一个Future对象通过该对象的get方法即可获取到任务执行的结果,详情可以查看(Java中的Callable的返回值是怎么来的),如果任务没有执行完,那么调用该方法的线程会被挂起,直到任务执行完毕才会被唤醒。这种模式背后的原理其实也是生产者消费者。你品,你细品:-)