JUC并发编程(7):阻塞队列--BlockingQueue

660 阅读8分钟

阻塞队列--BlockingQueue

参考:

1、队列(Queue)

Queue是数据结构中比较重要的一种类型,它支持FIFO,尾部添加、头部删除(先进队列的元素显出队列)。

Queue与List、Set同一级别,都是继承了Collection接口。

Queue可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。有两个端头的队列, 即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6中引人了 Deque 接口,并由 ArrayDequeLinkedList 类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

image.png

2、非阻塞队列(AbstractQueue)

2.1、优先级队列(priority queue)

元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。

堆是一个可以自我调整的二叉树,对树执行添加(add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。与 TreeSet—样,一个优先级队列既可以保存实现了 Comparable 接口的类对象, 也可以保存在构造器中提供的 Comparator 对象。使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将 1 设为“ 最高” 优先级,所以会将最小的元素删除)。

2.2、ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改。

ConcurrentLinkedQueuehead节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。

3、阻塞队列(BlockingQueue)

3.1、阻塞队列概述

什么是阻塞队列?

阻塞队列,顾名思义,首先它是一个队列,即先进先出,通过一个共享的队列,可以使得数据从队列的一端输入,从另一端输出,而一个阻塞队列在数据结构中所起的作用大致如图所示

image.png

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞,因为会等待生产者线程生产元素
  • 当阻塞队列是满时,往队列中添加元素的操作将会被阻塞
  • 同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他线程从队列中移除一个或者多 个元素或者全清空队列后使队列重新变得空闲起来并后续新增

所谓阻塞:即在某些情况下回挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被自动唤醒

阻塞队列的好处

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。 什么时候使用:多线程并发处理,线程池。

3.2、阻塞队列种类

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值 Integer.MAX_VALUE)阻塞队列,此队列按照先进先出的原则对元素进行排序。 吞吐量通常要高于 ArrayBlockingQueue。
  • SynchronousQueue:不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素
    • SynchronousQueue没有容量,与其他BlcokingQueue不同,SynchronousQueue是一个不存储元素的BlcokingQueue
    • 每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列.
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。

3.2.1、ArrayBlockingQueue

ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据(实际上可看作一个循环数组)。ArrayBlockingQueue 是一个有界队列,有界也就意味着,它不能够存储无限多数量的对象。所以在创建 ArrayBlockingQueue 时,必须要给它指定一个队列的大小。

此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平地访问队列,所谓公平访问队列是指阻塞的线程,可按照阻塞的先后顺序访问队列。非公平性是对先等待的线程是不公平的,当队列可用时,阻塞的线程都可以竞争访问队列的资格。

ArrayBlockingQueue fairQueue = new  ArrayBlockingQueue(1000,true);

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

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();
}

下面是ArrayBlockingQueue中的几个重要的方法:

add(E e):把 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则报异常 

offer(E e):表示如果可能的话,将 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则返回 false 

put(E e):把 e 加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续

poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null 

take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 Blocking 有新的对象被加入为止 

remainingCapacity():剩余可用的大小。等于初始容量减去当前的 size

注意:

  • ArrayBlockingQueue是先进先出队列
  • ArrayBlockingQueue是有界队列(即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞进队操作;容量空,则阻塞出队操作)
  • ArrayBlockingQueue不支持空元素
  • ArrayBlockingQueue 进队操作采用了加锁的方式保证并发安全。源代码里面有一个 while() 判断:
public void put(E e) throws InterruptedException {
    checkNotNull(e); // 非空判断
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 获取锁
    try {
        while (count == items.length) {
            // 一直阻塞,知道队列非满时,被唤醒
            notFull.await();
        }
        enqueue(e); // 进队
    } finally {
        lock.unlock();
    }
}
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    checkNotNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            // 阻塞,知道队列不满
            // 或者超时时间已过,返回false
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

JDK 文档提到的几点:

  • 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) 有可能失败(抛出一个异常)。

4、BlockingQueue的核心方法

4.1、概述

阻塞队列提供了四种处理方法:

方法/处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add()offer(e)put()offer(e,time, unit)
移除方法remove()poll()take()poll(time,unit)
检查方法element()peek()不可用不可用
  • 抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出llegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
  • 返回特殊值: 插入方法会返回是否成功,成功则返回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();
    }
}

4.2、阻塞队列api之抛出异常组

public class BlockingQueueDemo {

    public static void main(String[] args) {
        // List list = new ArrayList();
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        
        // 队列的界限是3,当使用add方法添加元素时,会抛异常,Exception in thread "main" java.lang.IllegalStateException: Queue full
        // System.out.println(blockingQueue.add("x"));

        // 检查队列中是否存在元素,存在时返回首个元素,当不存在元素时抛出异常,Exception in thread "main" java.util.NoSuchElementException
        System.out.println(blockingQueue.element());

        // 移除首个元素,返回被移除的元素
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        // 移除指定元素,成功返回true,失败返回false
        System.out.println(blockingQueue.remove("a"));
        System.out.println(blockingQueue.remove());
        // 当队列中没有元素时,调用remove方法会抛异常,Exception in thread "main" java.util.NoSuchElementException
        //System.out.println(blockingQueue.remove());
    }
}

4.3、阻塞队列api之返回特殊值组

public class BlockingQueueDemo {
    public static void main(String[] args) {
        
        // List list = new ArrayList();
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        // 添加元素,成功返回true;失败返回false,不抛出异常
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("x"));

        // 检查队列中是否有元素,有元素时,返回首元素的值
        System.out.println(blockingQueue.peek());

        // 移除元素时,成功返回移除的元素值;失败返回null,不抛出异常
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        
        // 检查队列中是否有元素,当队列为空时,返回null,不抛出异常
        System.out.println(blockingQueue.peek());
    }
}    

4.4、阻塞队列api之阻塞和超时控制

阻塞:

public class BlockingQueueDemo {

    public static void main(String[] args) throws Exception {
        
        // List list = new ArrayList();
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        // 不能存储 null 值
        blockingQueue.put("a");
        blockingQueue.put("a");
        blockingQueue.put("a");
        
        //当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据或响应中断退出
        //blockingQueue.put("a");

        System.out.println("========================>>");

        blockingQueue.take();
        blockingQueue.take();
        blockingQueue.take();
        // 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
        // blockingQueue.take();
    }
}    

超时:

public class BlockingQueueDemo {

    public static void main(String[] args) throws Exception {
        // List list = new ArrayList();
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
        
        //当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
        System.out.println(blockingQueue.offer("a", 2L, TimeUnit.SECONDS));
    }
}    

5、阻塞队列之同步SynchronnusQueue

SynchronousQueue 没有容量,与其他BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

代码证明:

public class SynchronousQueueDemo {

    public static void main(String[] args) {

        BlockingQueue blockingQueue = new SynchronousQueue();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"\tput 1");
                blockingQueue.put("1");

                System.out.println(Thread.currentThread().getName()+"\tput 2");
                blockingQueue.put("2");

                System.out.println(Thread.currentThread().getName()+"\tput 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\ttake 1");
                blockingQueue.take();

                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\ttake 2");
                blockingQueue.take();

                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName()+"\ttake 3");
                blockingQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }
}

控制台打印结果:

AAA	put 1
BBB	take 1
AAA	put 2
BBB	take 2
AAA	put 3
BBB	take 3

6、使用场景

  • 生产者消费者模式
    • 传统版
    • 阻塞队列版
  • 线程池
  • 消息中间件

6.1、生产者消费者模式

6.1.1、线程通信之生产者消费者传统版

步骤:

  • 线程操作(方法) 资源类
  • 判断 干活 通知
  • 防止虚假唤醒机制
  • Object 类的 wait() 方法可能存在虚假唤醒的情况,JDK1.8API文档原文如下,建议使用 while
  • 当只有2个线程时,使用 if 判断时,不会产生虚假唤醒的情况;但是线程数扩到4时,就会产生虚假唤醒的情况。

代码实现

/**
 * 题目:一个初始值为零的变量,两个线程对其交替操作,一个加1一个减1,来5轮
 */
class ShareData{

    private Integer number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment(){
        // 1,上锁
        lock.lock();
        try {
            // 判断
            while (number != 0) {
                condition.await();
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName()+"\t" + number);
            // 唤醒
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void decrement(){
        // 1,上锁
        lock.lock();
        try {
            // 判断
            while (number == 0) {
                condition.await();
            }
            // 干活
            number--;
            System.out.println(Thread.currentThread().getName()+"\t" + number);
            // 唤醒
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

}

public class ProdConsumer_TraditionDemo {

    public static void main(String[] args) {
        ShareData shareData = new ShareData();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.increment();
            }
        }, "A").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.decrement();
            }
        }, "B").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.increment();
            }
        }, "C").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                shareData.decrement();
            }
        }, "D").start();
    }
}

如果判断时,把 while 换成 if 可能会产生虚假唤醒的情况,控制台部分输出结果如下:

A	1
B	0
A	1
B	0
A	1
B	0
A	1
B	0
A	1
C	2
B	1
D	0

6.1.2、生产者消费者模式之阻塞队列版

  • 生产者-消费者问题描述:
    • 生产者和消费者在同一时间段内共用同一个存储空间;
    • 生产者往存储空间中添加产品,消费者从存储空间中取走产品;
    • 当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
  • 程序业务描述:
    • 生产者有序生产,消费者有序消费
    • 可以通过变量控制该模式启动或者停止
    • 生产者产能不足,消费者消费物品时,如果从队列里取不到物品,等待2s,超时则退出该模式
    • 生产者生产太多,消费者消费不完,生产者阻塞。
    • 库房内存为10

资源类:

package test;

import org.springframework.util.StringUtils;

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

public class Resource {
    
    //该标记变量标记是否开启 “生产-消费”模式,true表示开启,false表示关闭
    private volatile boolean flag = true;
    //定义变量去计算 生产-消费 物品的个数 需要保证原子性,所以使用该类型变量
    private AtomicInteger atomicInteger = new AtomicInteger();
    //定义阻塞队列 以构造方法传入具体的队列形式
    BlockingQueue<String> blockingQueue = null;

    public Resource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println("***传入的阻塞队列是:" + blockingQueue.getClass().getName());
    }

    //定义生产方法
    public void myProd() throws Exception {
        String data = null;
        boolean retValue;
        while (flag) {
            data = atomicInteger.incrementAndGet() + "";
            retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if (retValue) {
                System.out.println(Thread.currentThread().getName() + "\t" + "插入队列" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t" + "插入队列" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "生产动作结束");
    }

    //定义消费方法
    public void myConsume() throws Exception {
        String result = null;
        while (flag) {
            result = blockingQueue.poll(2L, TimeUnit.SECONDS);
            //说明取出失败,超时之后将退出生产-消费模式
            if (StringUtils.isEmpty(result)) {
                flag = false;
                System.out.println(Thread.currentThread().getName() + "\t" + "超过2s没有取到,退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "消费" + result + "成功");
            TimeUnit.SECONDS.sleep(2);
        }
    }

    public void stop() throws Exception {
        this.flag = false;
    }

}

测试类

package test;
 
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
 
/**
 * 使用阻塞队列模拟 生产者-消费者 模式
 * 生产者-消费者问题描述:
 * 1.生产者和消费者在同一时间段内共用同一个存储空间;
 * 2.生产者往存储空间中添加产品,消费者从存储空间中取走产品;
 * 3.当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
 * 程序业务描述:
 * 1.生产者有序生产,消费者有序消费
 * 2.可以通过变量控制该模式启动或者停止
 * 3.生产者产能不足,消费者消费物品时,如果从队列里取不到物品,等待2s,超时则退出该模式
 * 4.生产者生产太多,消费者消费不完,生产者阻塞。
 * 5.库房内存为10
 */
public class Main {
    public static void main(String[] args) throws Exception {
        //new 一个数组阻塞队列,初始化10
        Resource resource = new Resource(new ArrayBlockingQueue<>(10));
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "生产线程启动");
            try {
                resource.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A线程").start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "消费线程启动");
            System.out.println();
            System.out.println();
            try {
                resource.myConsume();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B线程").start();
        try {
            TimeUnit.SECONDS.sleep(5L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        resource.stop();
        System.out.println("5s时间到,生产-消费模式退出");
    }
}

结果