java阻塞队列详解-生产者消费者模式

1,162 阅读17分钟

什么是阻塞队列?

阻塞队列(BlockingQueue) 本质上还是一种队列,遵循先进先出,后进后出的原则,在此基础上,如果出队时阻塞队列为空,则会使当前线程陷入阻塞,直到入队新元素时通知线程继续执行,如果入队时阻塞队列为满,则会使当前线程陷入阻塞,直到出队旧元素时才通知线程进行执行。 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

简单来说与普通队列不同的是,他支持两个附加操作,即阻塞添加阻塞删除方法。

那么阻塞添加跟阻塞删除是什么意思呢?

阻塞11.png

如图线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。而在这一系列操作必须符合以下规定:

  • 阻塞添加:当阻塞队列是满时,往队列里添加元素的操作将被阻塞。
  • 阻塞移除:当阻塞队列是空时,从队列中 获取元素/删除元素的操作将被阻塞。

java里的阻塞队列

JDK7提供了7个阻塞队列。分别是

  • ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
  • DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue: 一个不存储元素的阻塞队列。
  • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

JDK提供的阻塞队列中,LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue

  • BlockingQueue接口: 单向阻塞队列实现了该接口。
  • BlockingDeque接口: 双向阻塞队列实现了该接口

注意事项:

SynchronousQueue: 队列只有一个元素,如果想插入多个,必须等队列元素取出后,才能插入,只能有一个“坑位”,用一个插一个。

需要注意的是LinkedBlockingQueue虽然是有界的,但有个巨坑,其默认大小是Integer.MAX_VALUE,高达21亿,一般情况下内存早爆了(在线程池的ThreadPoolExecutor有体现)。

对于 BlockingQueue 的阻塞队列提供了四种处理方法:

方法描述抛出异常返回特殊的值一直阻塞超时退出
插入数据add(e)offer(e)put(e)offer(e,time,unit)
获取并移除队列的头remove()poll()take()poll(time,unit)
获取但不移除队列的头element()peek()不可用不可用
  • 抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementEx·ception异常 。
  • 返回特殊值: 插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
  • 一直阻塞: 当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出: 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

   抛出异常返回特殊值 方法的实现是一样的,只不过对失败的操作的处理不一样!通过 AbstractQueue 的源码可以发现,add(e),remove(),element() 都是分别基于 offer(),poll(),peek() 实现的

public boolean add(E arg0) {
        if (this.offer(arg0)) {
            return true;
        } else {
            throw new IllegalStateException("Queue full");
        }
    }
 
    public E remove() {
        Object arg0 = this.poll();
        if (arg0 != null) {
            return arg0;
        } else {
            throw new NoSuchElementException();
        }
    }
 
    public E element() {
        Object arg0 = this.peek();
        if (arg0 != null) {
            return arg0;
        } else {
            throw new NoSuchElementException();
        }
    }

注意:

  • BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
  • BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE 的剩余容量。
  • BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
  • BlockingQueue 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection 操作(addAll、containsAll、retainAll 和 removeAll,这些方法尽可能地少使用)没有 必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll(c) 有可能失败(抛出一个异常)。

对于 BlockingDeque 的双向队列也提供了四种形式的方法

第一个元素(头部)

方法描述抛出异常返回特殊的值一直阻塞超时退出
插入数据addFirst(e)offerFirst(e)putFirst(e)offerFirst(e, time, unit)
获取并移除队列的头removeFirst()pollFirst()takeFirst()pollFirst(time, unit)
获取但不移除队列的头getFirst()peekFirst()不适用不适用

最后一个元素(尾部)

方法描述抛出异常返回特殊的值一直阻塞超时退出
插入数据addLast(e)offerLast(e)putLast(e)offerLast(e, time, unit)
获取并移除队列的头removeLast()pollLast()takeLast()pollLast(time, unit)
获取但不移除队列的头getLast()peekLast()不适用不适用

像所有 BlockingQueue 一样,BlockingDeque 是线程安全的,但不允许 null 元素,并且可能有(也可能没有)容量限制。

BlockingDeque 接口继承扩展了 BlockingQueue 接口,对于 继承自 BlockingQueue 的方法,除了插入方法(add、poll、offer方法,是插入的队列的尾部),其他方法,操作的都是队列的头部(第一个元素)。

七个阻塞队列的详细介绍

首先需要了解什么事有界队列什么事无界队列:

  • 有界队列: 就是有固定大小的队列。比如设定了固定大小的ArrayBlockingQueue,又或者大小为0,只是在生产者和消费者中做中转用的SynchronousQueue。
  • 无界队列:指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。

1、ArrayBlockingQueue

ArrayBlockingQueue是一个用数组实现的 *有界阻塞队列。 *此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平地访问队列 ,所谓公平访问队列是指阻塞的线程,可按照阻塞的先后顺序访问队列。非公平性是对先等待的线程是不公平的,当队列可用时,阻塞的线程都可以竞争访问队列的资格。 为了保证公平性,通常会降低吞吐量。

访问者的公平性是使用可重入锁实现的 ,代码如下:

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
}

2、LinkedBlockingQueue

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,此队列的默认和最大长度为integer.MAX_VALUE。按照先进先出的原则对元素进行排序。

3、PriorityBlockingQueue

是一个支持优先级的无界队列(虽然此队列逻辑上是无界,但是资源被耗尽的时候试图执行add操作也将失败,导致OutOfMemoryError)。默认情况下元素采取自然顺序排列(每个元素都必须事项Comparable接口)也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列.   其iterator() 方法中提供的迭代器并不 保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要有序地进行遍历,则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。    在此类上进行的操作不保证具有同等优先级的元素的顺序。 如果需要实施某一排序,那么可以定义自定义类或者比较器,比较器可使用修改键断开主优先级值之间的联系

package 组设队列;
​
public class Students11 {
    private String name;
    private Integer score;  //学生的成绩 ,  通过成绩的大小进行排序
​
    public Students11() {
    }
​
    public Students11(String name, Integer score) {
        this.name = name;
        this.score = score;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Integer getScore() {
        return score;
    }
​
    public void setScore(Integer score) {
        this.score = score;
    }
​
    @Override
    public String toString() {
        return "Students11{" +
                "name='" + name + ''' +
                ", score=" + score +
                '}';
    }
}

测试类:

public class demo11 {
    public static void main(String[] args) throws InterruptedException {
     System.out.println("======PriorityBlockingQueue===========");
        //PriorityBlockingQueue 阻塞队列
        //自定义排序规则
        PriorityQueue<Students11 > priorityQueue=new PriorityQueue<>(new Comparator<Students11>() {
            @Override
            public int compare(Students11 o1, Students11 o2) {
                return o1.getScore()-o2.getScore();
            }
        });
        //也可以通过lambda表达式
// PriorityQueue<Students11 > priorityQueue=new PriorityQueue<>((o1,o2)-> o1.getScore()-o2.getScore());
​
​
        //创建student对象
        Students11 s1=new Students11("张三",69);
        Students11 s2=new Students11("李四",76);
        Students11 s3=new Students11("王五",19);
        Students11 s4=new Students11("刘能",90);
        //向队列中添加student对象
        priorityQueue.add(s1);
        priorityQueue.add(s2);
        priorityQueue.add(s3);
        priorityQueue.add(s4);
        //获取,因为是一个对象,阻塞队列没有设置排序的规则,默认的排序规则是不可以的
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
    }
​
}

运行结果:

======PriorityBlockingQueue=========== Students11{name='王五', score=19} Students11{name='张三', score=69} Students11{name='李四', score=76} Students11{name='刘能', score=90}

4、DelayQueue

Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。注意 DelayQueue 的所有方法只能操作“到期的元素“ ,例如,poll()、remove()、size()等方法,都会忽略掉未到期的元素。 我们可以将DelayQueue运用在以下应用场景:

  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

DelayQueue 的实现是基于 PriorityQueue,是一个优先级队列,是以延时时间的长短进行排序的。所以,DelayQueue 需要知道每个元素的延时时间,而这个延时时间是由 Delayed 接口的 getDelay()方法获取的。所以, DelayQueue 的元素必须实现 Delay 接口;

//计算并返回延时时间
public long getDelay(TimeUnit unit) {
            return unit.convert(time - now(), TimeUnit.NANOSECONDS);
        }

延时队列的原理

延时队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程。

long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    else if (leader != null)
                        available.await();

实现案例:

package 组设队列;
​
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
​
public class Team implements Delayed {
    private  long time;  //触发的时间(失效时间点)
    //名称
    private String name ;
​
    public Team() {
    }
​
    public Team(String name,long time, TimeUnit unit) {
        this.time = System.currentTimeMillis() + (time > 0? unit.toMillis(time): 0);
        this.name = name;
    }
   //返回剩余的时间
    public long getTime(TimeUnit unit) {
        return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
​
    public void setTime(long time) {
        this.time = time;
    }
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    @Override
    public long getDelay(TimeUnit unit) {
        return 0;
    }
//    比较两个Delayed对象的大小, 比较顺序如下:
//   比较失效时间点, 先失效的返回-1,后失效的返回1
    @Override
    public int compareTo(Delayed o) {
        Team item = (Team) o;
        return this.time - item.time <= 0 ? -1 : 1;
    }
}

测试类:

import java.time.LocalDateTime;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
​
public class demo22 {
    public static void main(String[] args) {
        Team item1 = new Team("item1", 5, TimeUnit.SECONDS);
        Team item2 = new Team("item2",10, TimeUnit.SECONDS);
        Team item3 = new Team("item3",15, TimeUnit.SECONDS);
        DelayQueue<Team> queue = new DelayQueue<>();
        queue.put(item1);
        queue.put(item2);
        queue.put(item3);
        System.out.println("begin time:" + LocalDateTime.now());
        while(true){
            if(queue.size()<=0){
                break;
            }
            Team take = null;
            try {
                take = queue.take();
                System.out.format("name:{%s}, time:{%s}\n",take.getName(), LocalDateTime.now());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
​
        }
    }
    }

运行结果:

begin time:2023-11-03T14:27:42.093 name:{item1}, time:{2023-11-03T14:27:42.093} name:{item2}, time:{2023-11-03T14:27:42.108} name:{item3}, time:{2023-11-03T14:27:42.108}

5、SynchronousQueue

一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。

SynchronousQueue 的几个特点

  • 同步队列没有任何内部容量,甚至连一个队列的容量都没有。 所以很多继承的方法就没有用了,(如 isEmpty()始终返回true,size()为0,包含contain、移除remove 都始终为false 等等)。或者说,真正有意义的只有以下几个方法:获取并移除(poll()、poll(timeout,timeunit)、take())、插入(offer()、offer(timeout,timeunit)、put());
  • 适合于传递性设计,在这种设计中, 每一个put操作必须等待一个take操作,反之亦然 。(当然,如果用的是offer、poll的话,那么就不会阻塞等待)。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
  • 支持可选的公平排序策略。 默认情况下不保证这种排序。但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。
//设置公平性的构造方法
public SynchronousQueue(boolean fair) 
          创建一个具有指定公平策略的 SynchronousQueue。

6、LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的 无界阻塞TransferQueue队列 。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。

  • transfer方法: 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下:
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
  • tryTransfer方法: 则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法 ,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。

基本原理:

LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。

7、LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。和 LinkedBlockingQueue 一样,是有界的阻塞队列,默认长度以及最大长度是 Integer.MAX_VALUE。可在创建时,指定容量。

如何使用阻塞队列来实现生产者-消费者模型?

如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?

通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

使用BlockingQueue解决生产者消费者问题

为什么BlockingQueue适合解决生产者消费者问题

任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题。

Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程创建资源,另一个消费资源)。java.util.concurrent包下的BlockingQueue接口是一个线程安全的可用于存取对象的队列。

BlockingQueue是一种数据结构,支持一个线程往里存资源,另一个线程从里取资源。这正是解决生产者消费者问题所需要的,那么让我们开始解决该问题吧。

生产者:

package 组设队列;
​
import java.util.concurrent.BlockingQueue;
//生产者线程
public class Producer implements Runnable{
    private BlockingQueue<Object> queue;
​
    public Producer(BlockingQueue<Object> queue) {
        this.queue = queue;
    }
​
    @Override
    public void run() {
        while(true){
​
            try {
                //获取对象
                Object resourser = getResourser();
                //往阻塞队列中添加数据
                queue.put(resourser);
                System.out.println("生产者资源大小="+queue.size());
            } catch (InterruptedException e) {
                System.out.println("生产者读中断了");
            }
        }
    }
   Object getResourser(){
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           System.out.println("生产者读中断了");
       }
       return new Object();
   }
}

消费者:

package 组设队列;
​
import java.util.concurrent.BlockingQueue;
​
//消费者
public class Consumer implements Runnable{
    private BlockingQueue<Object> queue;
​
    public Consumer(BlockingQueue<Object> queue) {
        this.queue = queue;
    }
​
    @Override
    public void run() {
        while(true){
​
            try {
                //获取对象
                Object take = queue.take();
                System.out.println("消费者资源大小="+queue.size());
                getResourser(take);
                //往阻塞队列中添加数据
            } catch (InterruptedException e) {
                System.out.println("消费者中断了");
            }
        }
    }
    void getResourser(Object object){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            System.out.println("消费者中断了");
        }
        System.out.println("消费对象"+object);
    }
}

测试类:

package 组设队列;
​
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
​
public class test11 {
    public static void main(String[] args) {
        int numProducers = 4;
        int numConsumers = 3;
        BlockingQueue<Object> myqueue=new LinkedBlockingQueue<>(5);
​
        for (int i = 0; i < numProducers; i++) {
            new Thread(new Producer(myqueue)).start();
        }
        for (int i = 0; i < numConsumers; i++) {
            new Thread(new Consumer(myqueue)).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
​
​
    }
}

运行结果:

消费者资源大小=1 生产者资源大小=1 消费者资源大小=1 生产者资源大小=1 生产者资源大小=1 生产者资源大小=1 消费者资源大小=1 生产者资源大小=2 生产者资源大小=3 消费对象java.lang.Object@47a30dad 消费对象java.lang.Object@474d7160 生产者资源大小=5 生产者资源大小=4 消费者资源大小=3 消费对象java.lang.Object@37cdd5b0 消费者资源大小=4 消费者资源大小=2 生产者资源大小=3 生产者资源大小=5 消费对象java.lang.Object@439c9181 消费对象java.lang.Object@390a3d39 消费者资源大小=4 生产者资源大小=4 消费对象java.lang.Object@498a87f9 生产者资源大小=5 消费者资源大小=4 消费者资源大小=3 消费对象java.lang.Object@4db51a10 生产者资源大小=5 生产者资源大小=5 生产者资源大小=4

.......