java阻塞队列和多线程实现生产者-消费者模式(一对一、一对多、多对多)

笔者所有文章第一时间发布于:
hhbbz的个人博客

生产者-消费者模式是什么

  • 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
  • 这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。

为什么要使用生产者-消费者模式

  • 在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。

  • 在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

阻塞队列BlockingQueue的介绍

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

BlockingQueue的主要几种实现

  • ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。

  • LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。

  • PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。

通过简单的几个线程类实现

创建生产者类

/**
 * @author hhbbz on 2020-02-10.
 * @Explain:
 */
public class Producer implements Runnable{
    private BlockingQueue<Integer> queue;
    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        queue.offer(new Random().nextInt(100));
    }
}

创建消费者类

/**
 * @author hhbbz on 2020-02-10.
 * @Explain:
 */
public class Consumer implements Runnable{
    private BlockingQueue<Integer> queue;
    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }
​
    @Override
    public void run() {
        Integer value = queue.poll();
    }
}

测试入口类

/**
 * @author hhbbz on 2020-02-10.
 * @Explain:
 */
public class Main {
    public static void main(String[] args){
        //多个队列
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        BlockingQueue<Integer> queue2 = new LinkedBlockingQueue<>();
​
        //多个生产者
        Thread producer1 = new Thread(new Producer(queue));
        Thread producer2 = new Thread(new Producer(queue));
        Thread producer3 = new Thread(new Producer(queue2));
        Thread producer4 = new Thread(new Producer(queue2));
        producer1.start();
        producer2.start();
        producer3.start();
        producer4.start();
​
        //多个消费者
        Thread consumer1 = new Thread(new Consumer(queue));
        Thread consumer2 = new Thread(new Consumer(queue));
        Thread consumer3 = new Thread(new Consumer(queue2));
        Thread consumer4 = new Thread(new Consumer(queue));
        Thread consumer5 = new Thread(new Consumer(queue2));
        Thread consumer6 = new Thread(new Consumer(queue));
        consumer1.start();
        consumer2.start();
        consumer3.start();
        consumer4.start();
        consumer5.start();
        consumer6.start();
    }
}

自己想怎么处理生产者、消费者和队列之间的关系,都能很直观的进行调整。

接下来列一下项目中常用到的实现方式。

通过线程池封装起来的实现代码(!最重要最重要最重要!)

创建队列服务配置启动类,包含生产消息,可按需拆解

/**
 * @author hhbbz on 2020-02-10.
 * @Explain: 队列服务配置启动类,包含生产消息,可按需拆解
 */
@Component
@Slf4j
public class RecordQueueService {
    /**执行状态 */
    protected boolean isRunning;
    /**队列消费线程池 */
    private ThreadPoolExecutor executorService;
    //队列数量
    Integer queueNumber = 5;
    //队列长度
    Integer queueCapacity = 500;
    //每个队列对应多少个消费线程
    Integer singleQueueThreadNumber = 2;
    /**队列组列表 */
    List<BlockingQueue<Integer>> queueList = new ArrayList<>();
    //总线程数量,所有生产线程和消费线程
    Integer threadSize = queueNumber*singleQueueThreadNumber;

    public void start(String srvPoolName) {
        log.info("队列服务启动.......");
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("consume-"+srvPoolName+"-%d").build();

        //生产端线程和队列一对一
        for (int i = 0; i < queueNumber; i++) {
            queueList.add(new ArrayBlockingQueue<>(queueCapacity));
        }
        executorService = new ThreadPoolExecutor(
                threadSize, //线程池核心线程,至少要可以放入所有的生产线程和消费线程
                threadSize, //线程池容量大小
                300,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(threadSize+1),
                threadFactory
        );



        for (int i = 0; i < threadSize; i++) {
            //消费端
            //因为生产线程和队列一对一,通过getQueue取余的方式取到队列,即可实现多个消费线程消费同个队列
            executorService.submit(new SimpleRecordQueueHandler(this.getQueue(i),i));
        }
    }

    /**
     * 生产消息
     * @param str
     * @return
     */
    public Integer publish(String str) {
        if(!isRunning){
            //
        }
        BlockingQueue<Integer> queue = this.getQueue(str);
        try {
            if(queue!=null){
                queue.put(Integer.parseInt(str));
            }
        } catch (Exception e) {

        }
        if(queue!=null){
            return queue.size();
        }else{
            return 0;
        }

    }
    /**
     * 基于key值的hash值放在不同的队列里面
     * @param keyValue
     * @return
     */
    public BlockingQueue<Integer> getQueue(String keyValue){
        int p = keyValue.hashCode() % queueNumber;
        p = Math.abs(p);
        return getQueue(p);
    }

    //每个消费者对应的队列
    public BlockingQueue<Integer> getQueue(int position){
        position = position % queueList.size();
        if(position >= queueNumber || position <0){
            return queueList.get(0);
        }
        return queueList.get(position);
    }
}

创建消费类

/**
 * @author hhbbz on 2020-02-10.
 * @Explain: 消费消息类
 */
@Slf4j
public class SimpleRecordQueueHandler implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(SimpleRecordQueueHandler.class);

    //队列内容
    private Queue<Integer> data;

    //队列编号
    private int handlerNumber;

    public SimpleRecordQueueHandler(Queue<Integer> data, int handlerNumber) {
        this.data = data;
        this.handlerNumber = handlerNumber;
    }

    /**
     * 详细的业务逻辑处理
     */
    @Override
    public void run() {
        logger.info("当前消费队列编号:{}",handlerNumber);
        Integer value = data.poll();
        //TODO 消费逻辑
    }
}

总结

最后一种实现方式是较为常用的,建议加深印象多理解理解,生产者-消费者模式在实践中非常广泛和实用,灵活配置一对一,一对多更是可以画龙点睛。

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274

推荐阅读更多精彩内容