java多线程:生产者消费者实现的几种方式

本文探讨了Java中实现生产者消费者模式的四种方式,包括使用BlockingQueue、wait&notify、简单的LOCK&Condition以及优化后的并发更高的LOCK&Condition。通过分析,指出所有方法实质上都在实现或利用BlockingQueue。最后讨论了优化点,如使用CAS、拆解消费行为以及在特定场景下考虑多个队列策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.方式一:使用BlockingQueue


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by zs on 2019/8/19.
 */

public class MySampleProduceAndConsumer {
    BlockingQueue<Task> queue;

    AtomicInteger taskNo = new AtomicInteger(0);

    public MySampleProduceAndConsumer(int val){
        queue = new LinkedBlockingQueue<>(val);
    }
    public Runnable getConsumer(){
        return new Consumer();
    }
    public Runnable getProducer(){
        return new Producer();
    }

    class Consumer implements Runnable{
        @Override
        public void run(){
            //consume
            try {
                for (int i = 0; i < 50; i++) {
                    Task task = queue.take(); //关键代码1
                    System.out.println("cosume "+task.no);
                    Thread.sleep(5000);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }


        }
    }
    class Producer implements Runnable{
        @Override
        public void run() {
            //produce
            try {
                for (int i = 0; i < 50; i++) {
                    Thread.sleep((long)(1000*Math.random()));
                    Task task = new Task(taskNo.getAndIncrement());
                    queue.put(task);                                //关键代码2
                    System.out.println("produce " + task.no);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args){
        MySampleProduceAndConsumer mb = new MySampleProduceAndConsumer(3);
        new Thread(mb.getConsumer()).start();
        new Thread(mb.getProducer()).start();
    }
}

需要注意:

//入队
queue.add(task); // 入队的时候,如果队列已满,则抛出异常

queue.offer(task);// 如果队列已满,则直接丢弃,还会不停生产,直到消费者消费一个,挪出一个空位置,才会添加进来,其余的全部丢弃了。

queue.put(task);// 入队的时候,如果队列已满,则阻塞,直到消费一个,添加进来,继续生产,继续阻塞。
//出队
Task task = queue.take();//如果队列为空,则一直阻塞

Task task = queue.remove();//删除成功返回true,否则返回false

Task task = queue.poll();//如果队列为空,返回NULL,否则返回元素


2.wait &&notify 实现

如果不能直接只有阻塞队列,不能将并发与容量控制都封装在缓冲区中,就只能由消费者生产者内部完成,最简单的方法就是使用朴素的wait&&notify实现线程同步。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by zs on 2019/8/19.
 */
public class MyByWaitAndNotify {
    private final Object BUFFER_LOCK = new Object(); //使用Object对象锁
    Queue<Task> queue = new LinkedList<>(); //使用LinkedList自定义生产消费队列
    int cap; //定义queue的大小
    AtomicInteger taskNo = new AtomicInteger(0);
    public MyByWaitAndNotify(int val){
        this.cap = val;
    }
//get方法返回内部类实例,Runnable实例
    public Runnable getConsumer(){
        return new Consumer();
    }
    public Runnable getProducer(){
        return new Producer();
    }
//内部类实现Consumer
    private class Consumer implements Runnable{
        @Override
        public void run(){
            //consume
           try {
               synchronized (BUFFER_LOCK){  //关键代码块1
                   while (queue.size()==0){
                       BUFFER_LOCK.wait();
                   }
                   Task task = queue.poll();
                   System.out.println("consumer "+task.no);
                   BUFFER_LOCK.notifyAll();
                   Thread.sleep(5000);

               }
           }catch (InterruptedException e){
               e.printStackTrace();
           }
        }
    }
    //内部类实现Producer
    private class Producer implements Runnable{
        @Override
        public void run(){
            try {
                synchronized (BUFFER_LOCK){ //关键代码块2
                    Thread.sleep(900);
                    while (queue.size() == cap ){
                        BUFFER_LOCK.wait();
                    }
                    Task task = new Task(taskNo.getAndIncrement());
                    queue.offer(task);
                    System.out.println("producer "+task.no);
                    BUFFER_LOCK.notifyAll();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
//测试类
    public static void main(String[] args){
        MyByWaitAndNotify my = new MyByWaitAndNotify(3);
        for (int i = 0; i < 30; i++) {
            new Thread(my.getProducer()).start();
        }
        for (int i = 0; i < 30; i++) {
            new Thread(my.getConsumer()).start();
        }
    }
}

缺点:这种方式实现不灵活。

3. 简单的 LOCK&&Condition 实现

使用java.util.concurrent包下提供的 LOCK&& Condition

  • 该思路和方法二完全相同,只不过将synchronized换成了 Lock而已。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by zs on 2019/8/19.
 */
public class MyByLockAndCondition {
    private final Lock BUFFER_LOCK = new ReentrantLock();
    private final Condition BUFFER_CON = BUFFER_LOCK.newCondition();
    private Queue<Task> buffer = new LinkedList<>();
    private int cap;
    private AtomicInteger taskNo = new AtomicInteger(0);
    public MyByLockAndCondition(int val){
        this.cap = val;
    }

    public Runnable getConsumer(){
        return new Consumer();
    }
    public Runnable getProducer(){
        return new Producer();
    }

    private class Consumer implements Runnable{
        @Override
        public void run(){
            try {
                consume();//消费者线程 消费一次
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //consume方法
        public void consume() throws InterruptedException{
            BUFFER_LOCK.lockInterruptibly(); //加锁,由于要捕捉InterruptedException异常,故把consume方法单独拿出来写
            try {
                while (buffer.size()==0){
                    BUFFER_CON.await();
                }
                Task task = buffer.poll();
                Thread.sleep(1000);
                System.out.println("consumer "+task.no);
                BUFFER_CON.signalAll();  //唤醒线程,将等待队列线程放进同步队列
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                BUFFER_LOCK.unlock(); //释放锁放在finally块
            }
        }
    }

    private class Producer implements Runnable{
        @Override
        public void run(){
            try{
                produce();//生产者线程 生产一次
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        public void produce() throws InterruptedException{
            //produce
            BUFFER_LOCK.lockInterruptibly();//加锁
            try {
                while (buffer.size()==cap){
                    BUFFER_CON.await();
                }
                Task task = new Task(taskNo.getAndIncrement());
                System.out.println("produce "+task.no);
                buffer.offer(task);
                Thread.sleep(900);
                BUFFER_CON.signalAll();
            }finally{
                BUFFER_LOCK.unlock();
            }
        }
    }
    //测试
    public static void main(String[] args){
        MyByLockAndCondition my = new MyByLockAndCondition(3);
        for (int i = 0; i < 10; i++) {
            new Thread(my.getConsumer()).start();
        }
        for (int i = 0; i < 10; i++) {
            new Thread(my.getProducer()).start();
        }
    }

}

4.更高并发的lock&&Condition

前面的几种方法,会发现,方法1的并发性高于方法2和3,暂且不关心BlockingQueue的内部具体实现,现在来分析如何优化方法3.

  • 分析:方法3的瓶颈很明显,方法3只用了一个BUFFER_LOCK,同一时刻只有一个线程能访问buffer(消费者线程或者生产者线程)。而实际上,如果缓冲区是一个队列的话,“生产者入队”和“消费者出队”两个操作并没有直接的关系,可以同时进行。可以在队尾入队,在队首出队,这样性能可以提高两三倍。

  • 如何去掉这个瓶颈?需要两个锁:CONSUME_LOCK和PRODUCE_LOCK,相应地,也需要两个条件变量:NOT_EMPTY和NOT_FULL。这样消费者和生产者之间就可以并行。

public class LockConditionModel2 implements Model {
 private final Lock CONSUME_LOCK = new ReentrantLock();
 private final Condition NOT_EMPTY = CONSUME_LOCK.newCondition();
 private final Lock PRODUCE_LOCK = new ReentrantLock();
 private final Condition NOT_FULL = PRODUCE_LOCK.newCondition();
 private final Buffer<Task> buffer = new Buffer<>();
 private AtomicInteger bufLen = new AtomicInteger(0);
 private final int cap;
 private final AtomicInteger increTaskNo = new AtomicInteger(0);
 public LockConditionModel2(int cap) {
   this.cap = cap;
 }
 @Override
 public Runnable newRunnableConsumer() {
   return new ConsumerImpl();
 }
 @Override
 public Runnable newRunnableProducer() {
   return new ProducerImpl();
 }
 private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {
   @Override
   public void consume() throws InterruptedException {
     int newBufSize = -1;
     CONSUME_LOCK.lockInterruptibly();
     try {
       while (bufLen.get() == 0) {
         System.out.println("buffer is empty...");
         NOT_EMPTY.await();
       }
       Task task = buffer.poll();
       newBufSize = bufLen.decrementAndGet();
       if (newBufSize > 0) {
         NOT_EMPTY.signalAll();
       }
     } finally {
       CONSUME_LOCK.unlock();
     }
     if (newBufSize < cap) {
       PRODUCE_LOCK.lockInterruptibly();
       try {
         NOT_FULL.signalAll();
       } finally {
         PRODUCE_LOCK.unlock();
       }
     }
     
     assert task != null;
     // 固定时间范围的消费,模拟相对稳定的服务器处理过程
     Thread.sleep(500 + (long) (Math.random() * 500));
     System.out.println("consume: " + task.no);
   }
 }
 private class ProducerImpl extends AbstractProducer implements Producer, Runnable {
   @Override
   public void produce() throws InterruptedException {
     // 不定期生产,模拟随机的用户请求
     Thread.sleep((long) (Math.random() * 1000));
     int newBufSize = -1;
     PRODUCE_LOCK.lockInterruptibly();
     try {
       while (bufLen.get() == cap) {
         System.out.println("buffer is full...");
         NOT_FULL.await();
       }
       Task task = new Task(increTaskNo.getAndIncrement());
       buffer.offer(task);
       newBufSize = bufLen.incrementAndGet();
       System.out.println("produce: " + task.no);
       if (newBufSize < cap) {
         NOT_FULL.signalAll();
       }
     } finally {
       PRODUCE_LOCK.unlock();
     }
     if (newBufSize > 0) {
       CONSUME_LOCK.lockInterruptibly();
       try {
         NOT_EMPTY.signalAll();
       } finally {
         CONSUME_LOCK.unlock();
       }
     }
   }
 }
 private static class Buffer<E> {
   private Node head;
   private Node tail;
   Buffer() {
     // dummy node
     head = tail = new Node(null);
   }
   public void offer(E e) {
     tail.next = new Node(e);
     tail = tail.next;
   }
   public E poll() {
     head = head.next;
     E e = head.item;
     head.item = null;
     return e;
   }
   private class Node {
     E item;
     Node next;
     Node(E item) {
       this.item = item;
     }
   }
 }
 public static void main(String[] args) {
   Model model = new LockConditionModel2(3);
   for (int i = 0; i < 2; i++) {
     new Thread(model.newRunnableConsumer()).start();
   }
   for (int i = 0; i < 5; i++) {
     new Thread(model.newRunnableProducer()).start();
   }
 }

需要注意的是,由于需要同时在UnThreadSafe的缓冲区 buffer 上进行消费与生产,我们不能使用实现二、三中使用的队列了,需要自己实现一个简单的缓冲区 Buffer。Buffer要满足以下条件:

  • 在头部出队,尾部入队

  • 在poll()方法中只操作head

  • 在offer()方法中只操作tail

这4种写法的本质相同——都是在使用或实现BlockingQueue。

实现一直接使用BlockingQueue,实现四实现了简单的BlockingQueue,而实现二、三则实现了退化版的BlockingQueue(性能降低一半)。

还能进一步优化吗

我们已经优化掉了消费者与生产者之间的瓶颈,还能进一步优化吗?

如果可以,必然是继续优化消费者与消费者(或生产者与生产者)之间的并发性能。然而,消费者与消费者之间必须是串行的,因此,并发模型上已经没有地方可以继续优化了

仔细分析下,实现四中的signalAll都可以换成signal。这里为了屏蔽复杂性,回避了这个优化。

不过在具体的业务场景中,一般还能够继续优化。如:

  • 并发规模中等,可考虑使用CAS代替重入锁

  • 模型上不能优化,但一个消费行为或许可以进一步拆解、优化,从而降低消费的延迟

  • 一个队列的并发性能达到了极限,可采用“多个队列”(如分布式消息队列等)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值