java多线程之Producer-Consumer模式

23 篇文章 0 订阅
20 篇文章 0 订阅

一、Producer-Consumer设计模式

Producer是生产者的意思,指的是生产数据的线程,Consumer则是消费者的意思,指的是使用数据的线程。

生产者安全的将数据交给消费者,虽然仅是这样看似简单的操作,但生产者和消费者以不同的线程运行时,二者之间的处理速度就会引起差异。例如:消费者想消费数据,可生产者还没有生产出数据,或者生产者生产出了数据,可消费者还无法接受数据。

Producer-Consumer模式在生产者和消费者之间加入一个桥梁角色,改桥梁用于消除线程间处理速度的差异。一般来说,在该模式中,生产者和消费者都有多个。

二、示例程序

为了理解Producer-Consumer设计模式,我们先来看一个示例程序,在这个示例程序中,有3位蛋糕制作者制作蛋糕,并将蛋糕放到桌上,然后有3位客人来吃这些蛋糕;

  • 蛋糕制作者(MakerThread)制作蛋糕,并放到桌子上;
  • 桌上最多可以放置3个蛋糕;
  • 如果桌上已经放满了3个蛋糕,必须等到桌上空出位置才能继续放;
  • 客人(EaterThread)取桌上的蛋糕吃;
  • 客人按照蛋糕放置到桌上的顺序来取蛋糕;
  • 当桌上一个蛋糕都没有时,必须等桌上有蛋糕了才能继续取蛋糕吃。
类名说明
Main.java测试类
MakerThread.java蛋糕制作者的类
EaterThread.java蛋糕消费者
Table.java表示桌子的类

1.Main类

会创建一个桌子的实例,并启动表示糕点师和客人的线程。

package com.viagra.Producer_Consumer_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:18
 * @Description:
 */
public class Main {
    public static void main(String[] args) {
        Table table = new Table(3);
        new MakerThread("MakerThread-1", table, 31415).start();
        new MakerThread("MakerThread-2", table, 92653).start();
        new MakerThread("MakerThread-3", table, 58979).start();
        new EaterThread("EaterThread-1", table, 32384).start();
        new EaterThread("EaterThread-2", table, 62643).start();
        new EaterThread("EaterThread-3", table, 38327).start();
    }
}

2.MakerThread类

用户制作蛋糕,并将蛋糕放到桌上。MakerThread会先暂停一段随机时长,然后再调用Table类的put方法,将制作好的蛋糕放到桌上。

package com.viagra.Producer_Consumer_Pattern.Lesson1;

import java.util.Random;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:18
 * @Description:
 */
public class MakerThread extends Thread {
    private final Random random;
    private final Table table;
    private static int id = 0; //蛋糕的流水号

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

    public void run() {
        try {
            while (true) {
                Thread.sleep(random.nextInt(1000));
                String cake = "[ Cake No. " + nextId() + " by " + getName() + "]";
                table.put(cake);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static synchronized int nextId() {
        return id++;
    }
}

3.EaterThread类

表示从桌子上取蛋糕的客人,通过Table类的take方法取桌上的蛋糕,也暂停一段随机时长。

package com.viagra.Producer_Consumer_Pattern.Lesson1;

import java.util.Random;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:18
 * @Description:
 */
public class EaterThread extends Thread {
    private final Random random;
    private final Table table;

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

    public void run() {
        try {
            while (true) {
                String cake = table.take();
                Thread.sleep(random.nextInt(1000));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.Table类

用户表示放蛋糕的桌子,蛋糕以String类型表示,Table声明了一个String数据类型的字段,用于作为蛋糕的实际放置位置。

为了正确放置put和取take蛋糕,Table类还声明了int类型的字段tail,head和count:

  • tail字段,表示下一次放置put蛋糕的位置;
  • head字段,表示下一次取take蛋糕的位置;
  • count字段,表示当前桌子上蛋糕的个数。
package com.viagra.Producer_Consumer_Pattern.Lesson1;

/**
 * @Auther: viagra
 * @Date: 2019/11/19 17:18
 * @Description:
 */
public class Table {
    private final String[] buffer;
    private int tail;//下次put的位置
    private int head;//下次take的位置
    private int count;//buffer中的蛋糕数

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

    /**
     * put方法使用了Guarded Suspension模式,守护条件为while条件表达式的逻辑非运算!count >= buffer.length,也就是说当桌上放置的蛋糕小于最大
     * 放置量,把它作为放置蛋糕的put方法守护条件.
     * 通过上面的处理,蛋糕已经放置导桌上了,由于桌子的状态发生了变化,所以要执行notifyAll,唤醒所有正在wait的线程.
     *
     * @param cake
     * @throws InterruptedException
     */
    //放置蛋糕
    public synchronized void put(String cake) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " puts " + cake);
        while (count >= buffer.length) {
            wait();
        }
        buffer[tail] = cake;
        tail = (tail + 1) % buffer.length;
        count++;
        notifyAll();
    }

    /**
     * take方法使用了Guarded Suspension模式,守护条件为!count <= 0.
     *
     * @return
     * @throws InterruptedException
     */
    //拿取蛋糕
    public synchronized String take() throws InterruptedException {
        while (count <= 0) {
            wait();
        }
        String cake = buffer[head];
        head = (head + 1) % buffer.length;
        count--;
        notifyAll();
        System.out.println(Thread.currentThread().getName() + " takes " + cake);
        return cake;
    }
}

5.运行结果

MakerThread-2 puts [ Cake No. 0 by MakerThread-2]
EaterThread-2 takes [ Cake No. 0 by MakerThread-2]
MakerThread-1 puts [ Cake No. 1 by MakerThread-1]
EaterThread-1 takes [ Cake No. 1 by MakerThread-1]
MakerThread-3 puts [ Cake No. 2 by MakerThread-3]
EaterThread-3 takes [ Cake No. 2 by MakerThread-3]
MakerThread-2 puts [ Cake No. 3 by MakerThread-2]
EaterThread-1 takes [ Cake No. 3 by MakerThread-2]
MakerThread-3 puts [ Cake No. 4 by MakerThread-3]
EaterThread-3 takes [ Cake No. 4 by MakerThread-3]
MakerThread-1 puts [ Cake No. 5 by MakerThread-1]
EaterThread-2 takes [ Cake No. 5 by MakerThread-1]
MakerThread-2 puts [ Cake No. 6 by MakerThread-2]
EaterThread-1 takes [ Cake No. 6 by MakerThread-2]
MakerThread-3 puts [ Cake No. 7 by MakerThread-3]
EaterThread-3 takes [ Cake No. 7 by MakerThread-3]
MakerThread-1 puts [ Cake No. 8 by MakerThread-1]
EaterThread-1 takes [ Cake No. 8 by MakerThread-1]
MakerThread-2 puts [ Cake No. 9 by MakerThread-2]
EaterThread-3 takes [ Cake No. 9 by MakerThread-2]
MakerThread-1 puts [ Cake No. 10 by MakerThread-1]
EaterThread-3 takes [ Cake No. 10 by MakerThread-1]
MakerThread-1 puts [ Cake No. 11 by MakerThread-1]
EaterThread-1 takes [ Cake No. 11 by MakerThread-1]
MakerThread-3 puts [ Cake No. 12 by MakerThread-3]
EaterThread-2 takes [ Cake No. 12 by MakerThread-3]
MakerThread-3 puts [ Cake No. 13 by MakerThread-3]
EaterThread-3 takes [ Cake No. 13 by MakerThread-3]
MakerThread-2 puts [ Cake No. 14 by MakerThread-2]
EaterThread-3 takes [ Cake No. 14 by MakerThread-2]
MakerThread-2 puts [ Cake No. 15 by MakerThread-2]
EaterThread-2 takes [ Cake No. 15 by MakerThread-2]
MakerThread-3 puts [ Cake No. 16 by MakerThread-3]
EaterThread-3 takes [ Cake No. 16 by MakerThread-3]
MakerThread-1 puts [ Cake No. 17 by MakerThread-1]
EaterThread-1 takes [ Cake No. 17 by MakerThread-1]
MakerThread-1 puts [ Cake No. 18 by MakerThread-1]
EaterThread-2 takes [ Cake No. 18 by MakerThread-1]
MakerThread-3 puts [ Cake No. 19 by MakerThread-3]
EaterThread-2 takes [ Cake No. 19 by MakerThread-3]

6.示例程序的时序图

img

三、Producer-Consumer模式中登场角色

  • Data:由Producer角色生成,共Consumer角色使用。
  • Profucer角色生产Data角色,并将其传给Channel(通道)角色。
  • Consumer角色从Channel(通道)角色获取Data角色并且使用。
  • Channel角色从Producer角色获取Data,还会响应Consumer的请求,传递Data,为了确保安全性,Channel会对Profucer和Consumer访问执行互斥处理。

如图:

img

四、扩展思路

1.守护安全性的Channel角色(可复用性)

在Producer-Consumer模式中,承担安全守护责任的是Channel角色,Channel执行线程间的互斥处理,确保Producer正确地将Data传递给Consumer。

2.不可以直接传递吗?

Producer-Consumer模式为了从Producer正确地将Data传递给

(1)直接调用方法,如果Producer直接调用Consumer的方法,那么执行处理的就不是Consumer的线程,而是Producer的线程,这样执行处理的时间就必须由Producer的线程来承担,准备下一个数据的处理也会发生延迟,程序性能会变差。就好比:蛋糕师做好蛋糕,直接交给客人,在客人吃完后再做下一个蛋糕一样。

(2)插入Channel角色,Producer将Data传递给Channel后,无需等待就可以准备下一个Data了,Consumer吃蛋糕的线程完全不会影响Producer线程。

3.Channel的剩余空间所导致的问题

4.以什么顺序传递给Data?

(1)队列-先接收的先传递

最通常的方法就是先接收先传递,这种方法成为FIFO,先进先出或者队列。

(2)栈-后接收的先传递

与队列相反,该方法是后接收的先传递,我们将这种方法成为LIFO(last in frist out),后进先出或者栈。如:叠放碟子的场景等。

(3)优先队列-“优先”的先传递

比较常用的方法还有优先队列,channel给接收到的Data赋予优先级,优先级高的先被传递出去,而优先级的确定方法千差万别。

5.“存在中间角色”的意义

Channel最为中间角色具有非常重要的意义,可以实现线程的协调运行。

  • 线程的协调运行要考虑“放在中间的东西”;
  • 线程的互斥处理要考虑“应该保护的东西”。

协调运行和互斥处理其实是内外统一的,为了让线程协调运行,必须执行互斥处理,以防止共享的内容被破坏。而线程的互斥处理是为了线程的协调运行才执行的。因此,协调运行和互斥处理之间有着很深的关系。

代码案例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值