生产者-消费者模式

知其然还要知其所以然,为了解决什么问题才构建了这一模型?什么情况下使用该模型?

关于生产者消费者问题:

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

如何解决该问题?

首先来分析下:缓冲区为空则禁止消费者消费,反之禁止生产者生产,同种行为之间互斥。

解决办法:使用两种锁,相应角色的线程需要获取到对应的锁才可以执行,每次执行完后都会唤醒对方阻塞的线程,执行前都需要对缓冲区状态进行判断,不满足则阻塞等待直到被唤醒重新获取锁执行。(当然简单点的话我们可以使用一个锁,锁住整个缓冲区,任何线程彼此互斥)

关于Java实现:1,利用synchronized 和 wait ,notify实现上述所说的一个锁的解决方法。2,利用ReentrentLock 和 Condition 来实现两个锁的解决方法。3,利用BlockingQueue,这种方式最简单,双方持有同一个BlockingQueue对象,不需要任何其它同步操作。关于JUC下不同的BlockingQueue实现原理,请看我关于它们的解析文章

一个例子:蛋糕师做蛋糕,在这个例子里我们使用一个锁,锁住整个缓冲区,任何线程彼此之间互斥,

  • 蛋糕师(MakerThreaed)制作蛋糕(String),并将其放在桌子上(Table)
  • 桌子上最多放三个
  • 当桌子上放满三个了,就必须等待直到有空闲位置
  • 客人(EaterThread)从桌上取蛋糕
  • 按照蛋糕被放置在桌上的顺序来取
  • 若桌子为空则需等待,直到新蛋糕被放置在桌上
名字 说明
Main 测试类
MakerThread 糕点师
EaterThread 客人
Table 桌子
 public class Main {
    public static void main(String[] args) {
        Table table = new Table(3);
        Thread[] threads = {
                new MakerThread("MakerThread-1", table, 31415),
                new MakerThread("MakerThread-2", table, 92653),
                new MakerThread("MakerThread-3", table, 45862),
                new EaterThread("EaterThread-1", table, 75128),
                new EaterThread("EaterThread-2", table, 56928),
                new EaterThread("EaterThread-3", table, 15648),
        };
        for (Thread thread : threads){
            thread.start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }

        System.out.println("*******interrupt*******");
		// 中断所有线程
        for (Thread thread : threads){
            thread.interrupt();
        }
    }
}

蛋糕是这样的:[ Cake No:xxx by MakerThread-x ]

public class MakerThread extends Thread {

    private final Random random;
    private final Table table;
    private static int id = 0; // 蛋糕流水号(所有蛋糕师共用)

    public MakerThread(String name, Table table, long seed) {
        super(name);
        this.random = new Random(seed);
        this.table = table;
    }

    @Override
    public void run() {
        try {
            while (true){
	            // 随机停顿0-1000毫秒,代表之作蛋糕的耗时
                Thread.sleep(random.nextInt(1000));
                // 这就是蛋糕 [ Cake No:xxx by MakerThread-x ]
                String cake = "[ Cake No:" + nextId() + " by " + getName() + " ]";
                table.put(cake);
            }
        } catch (InterruptedException e) {
        }
    }
	
	// 由于id是共用的所以用static synchronized,锁为类对象
    private static synchronized int nextId(){
        return id++;
    }
}

EaterThreader

public class EaterThread extends Thread {

    private final Random random;
    private final Table table;

    public EaterThread(String name, Table table, long seed) {
        super(name);
        this.random = new Random(seed);
        this.table = table;
    }

    @Override
    public void run() {
        try {
            while (true){
	            // 取蛋糕
                table.take();
                // 吃蛋糕
                Thread.sleep(random.nextInt(1000));
            }
        } catch (InterruptedException e) {
        }
    }
}

Table

public class Table {
    private final String[] buffer; //一个存放蛋糕的数组
    private int head; // 下次put的位置
    private int tail; //下次take的位置
    private int count; // 桌子上蛋糕的个数

    public Table(int count) {
        this.buffer = new String[count];
        this.head = 0;
        this.tail = 0;
        this.count = 0;

	// 放置蛋糕
    public synchronized void put(String cake) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " puts " + cake);
        while (count >= buffer.length){
            System.out.println(Thread.currentThread().getName() + " wait BEGIN");
            wait();
            System.out.println(Thread.currentThread().getName() + " wait END");
        }
        buffer[tail] = cake;
        tail = (tail + 1) % buffer.length;
        count++;
        notifyAll();
    }
    
	// 拿走蛋糕
    public synchronized String take() throws InterruptedException {
        while (count <= 0) {
            System.out.println(Thread.currentThread().getName() + " wait BEGIN");
            wait();
            System.out.println(Thread.currentThread().getName() + " wait END");
        }
        String cake = buffer[head];
        head = (head + 1) % buffer.length;
        count--;
        notifyAll();
        System.out.println(Thread.currentThread().getName() + " take " + cake);
        return cake;
    }
}

来看看执行结果

EaterThread-1 wait BEGIN
EaterThread-3 wait BEGIN
EaterThread-2 wait BEGIN
MakerThread-2 puts [ Cake No:0 by MakerThread-2 ]
EaterThread-2 wait END
EaterThread-2 take [ Cake No:0 by MakerThread-2 ]
EaterThread-3 wait END
EaterThread-3 wait BEGIN
EaterThread-1 wait END
EaterThread-1 wait BEGIN
MakerThread-1 puts [ Cake No:1 by MakerThread-1 ]
EaterThread-1 wait END
EaterThread-1 take [ Cake No:1 by MakerThread-1 ]
EaterThread-3 wait END
EaterThread-3 wait BEGIN
EaterThread-1 wait BEGIN
EaterThread-2 wait BEGIN
MakerThread-2 puts [ Cake No:2 by MakerThread-2 ]
EaterThread-2 wait END
EaterThread-2 take [ Cake No:2 by MakerThread-2 ]
EaterThread-1 wait END
EaterThread-1 wait BEGIN
EaterThread-3 wait END
EaterThread-3 wait BEGIN
MakerThread-3 puts [ Cake No:3 by MakerThread-3 ]
EaterThread-3 wait END
EaterThread-3 take [ Cake No:3 by MakerThread-3 ]
EaterThread-1 wait END
EaterThread-1 wait BEGIN
MakerThread-3 puts [ Cake No:4 by MakerThread-3 ]
EaterThread-1 wait END
EaterThread-1 take [ Cake No:4 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:5 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:6 by MakerThread-3 ]
MakerThread-2 puts [ Cake No:7 by MakerThread-2 ]
EaterThread-1 take [ Cake No:5 by MakerThread-1 ]
EaterThread-2 take [ Cake No:6 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:8 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:9 by MakerThread-3 ]
EaterThread-3 take [ Cake No:7 by MakerThread-2 ]
EaterThread-3 take [ Cake No:8 by MakerThread-1 ]
MakerThread-2 puts [ Cake No:10 by MakerThread-2 ]
MakerThread-3 puts [ Cake No:11 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:12 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-1 take [ Cake No:9 by MakerThread-3 ]
MakerThread-1 wait END
EaterThread-3 take [ Cake No:10 by MakerThread-2 ]
MakerThread-1 puts [ Cake No:13 by MakerThread-1 ]
EaterThread-3 take [ Cake No:11 by MakerThread-3 ]
EaterThread-2 take [ Cake No:12 by MakerThread-1 ]
EaterThread-1 take [ Cake No:13 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:14 by MakerThread-3 ]
MakerThread-3 puts [ Cake No:15 by MakerThread-3 ]
MakerThread-2 puts [ Cake No:16 by MakerThread-2 ]
MakerThread-2 puts [ Cake No:17 by MakerThread-2 ]
MakerThread-2 wait BEGIN
EaterThread-3 take [ Cake No:14 by MakerThread-3 ]
MakerThread-2 wait END
MakerThread-3 puts [ Cake No:18 by MakerThread-3 ]
MakerThread-3 wait BEGIN
MakerThread-1 puts [ Cake No:19 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-2 take [ Cake No:15 by MakerThread-3 ]
MakerThread-1 wait END
MakerThread-3 wait END
MakerThread-3 wait BEGIN
EaterThread-1 take [ Cake No:16 by MakerThread-2 ]
MakerThread-3 wait END
MakerThread-3 puts [ Cake No:20 by MakerThread-3 ]
MakerThread-3 wait BEGIN
MakerThread-1 puts [ Cake No:21 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-3 take [ Cake No:17 by MakerThread-2 ]
MakerThread-1 wait END
MakerThread-3 wait END
MakerThread-3 wait BEGIN
*******interrupt*******

来分析这些登场的角色

Data
在生产者消费者中,Data角色由Producer角色生成,供Consumer角色使用。在上面例子中就是蛋糕(String)。
Producer生产者
Producer角色生成Data角色,并将其传递给Channel角色。上面例子的MakerThread就是Producer。
Consumer消费者
Consumer角色从Channel中获取Data并使用。上面例子里就是EaterThread。
Channel通道
Channel保管从Producer获取的Data,响应Consumer的请求传递Data。为了为了确保安全,Channel会对Producer和Consumer的访问执行互斥处理。
当Producer将Data传递给Channel,如果Channel状态不适合接受Data,那么Producer将一直等待,直到Channel可以接受为止。
当Consumer从Channel获取Data时,若没有则需一直等待,直到有可获取的Data为止。
存在多个Producer与Consumer时,执行互斥以避免互相之间的影响,它的存在实现了线程的线条运行。
由此可看出Channel承担着传递Data的任务。
对应上面的例子中的Table。

生产者-消费者模式_第1张图片

如果不通过Channel直接传递呢?Channel起到通道,中转站的作用
,如果Producer直接调用Consumer的方法,不通过Channel,这样就相当于Producer线程承担了处理所花费的时间,用上面的例子来说就相当于蛋糕师做出一个蛋糕亲手交给客人,然后必须等待客人吃完才能继续做下一个蛋糕,这样程序的相应性变得很差。
而Channel这种方式,Producer便可以不用考虑Consumer,可以持续不断创建Data,当然有存储空间上的限制。

关于wait,需要注意锁的问题,线程进入等待队列时已经释放了锁,当正在wait的线程被调用interrupt方法时,该线程会在重新获取锁后抛出InterruptedEcxception异常,在获取锁之前是不会抛异常的。
关于Thread.interrupted()方法,他是个静态方法,检查并清除中断状态,它的操作对象是当前线程,不能用于清除其它线程的中断状态。

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

该系列文章是《图解Java多线程设计模式》的读书笔记

你可能感兴趣的:(JUC)