BlockingQueue阻塞队列

首先来看扩展关系图:

BlockingQueue是一个接口,其底下的实现类有很多,其中ArrayBlockingQueue和LinkedBlockingQueue是常用的,另外SynchronousQueue同步队列也是重点,故此本文只对这3个队列进行概述。

▶ BlockingQueue 是什么?

一个阻塞队列,是Java util.concurrent包下重要的数据结构,提供了线程安全的队列访问方式。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

▶ BlockingQueue的实现类有:

参数描述
ArrayBlockingQueue数组结构的有界阻塞队列。创建必须指定大小
LinkedBlockingQueue链表结构的有界阻塞队列。创建未指定大小,默认为Integer.MAX_VALUE-无界
SynchronousQueue无界,队列大小为0,而最大创建线程数:Integer.MAX_VALUE , 线程不足,会一直创建新线程(交给线程处理,不存入队列中)
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列。
DelayQueue一个使用优先级队列实现的无界阻塞队列,即在延迟期满时才能从中提取元素
LinkedTransferQueue链表结构的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque链表结构的无界双向阻塞队列。基于链表的先进先出队列,如创建时未指定队列大小,则默认为Integer.MAX_VALUE;

扩展一个知识点: Integer.MAX_VALUE (约21亿),假设你的队列大小为此值,在并发情况下,如果存在大量请求打过来,你的处理速度远跟不上任务的请求数量,就会导致队列的请求任务堆积,在达到21亿的请求任务数前,你的内存可能早就爆炸了,从而造成OOM。

▶ BlockingQueue的操作方法:

阻塞队列提供了4种API

方法抛出异常不抛异常,有返回值阻塞等待超时等待
添加addofferputoffer (Timeout)
移除removepolltakepoll (Timeout)
检测队首元素elementpeek//

下面将用代码来说明上述阻塞队列的 4种处理方式

方式一:抛出异常

/**
  抛出异常
  add:超出队列指定容量,会报:Queue full 异常
  remove:(每调用一次,移除一个元素),队列为空继续remove会报:NoSuchElementException
*/
public static void main(String[] args){
    // 1. 定义一个队列容量为2,基于数组的阻塞队列
    ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
  
    // 2. 添加元素  true:添加成功   false:添加失败
    System.out.println(queue.add("1"));
    System.out.println(queue.add("2"));
    //queue.add("3"); // 使用add超出队列指定容量,会报:Queue full异常

    // 3. 获取队列首部元素 :1
    System.out.println(queue.element());

    // 4.移除元素
    //队列为空继续用remove:NoSuchElementException
    System.out.println("移除元素:"+queue.remove());

    System.out.println("队列:"+queue);
}

执行结果:

我们把 // queue.add("3"); 的注释打开:添加第3个元素时,报Queue full异常

方式二:不抛异常,有返回值

/**
    有返回值,没有异常
    offer:返回false,超出容量后不再添加入队
    poll:(每调用一次,移除一个元素)队列为空,继续poll会输出null
*/
public static void main(String[] args){
    // 1.定义队列容量为2
    ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
   
    // 2.添加元素
    System.out.println(queue.offer("3"));
    System.out.println(queue.offer("4"));
    System.out.println(queue.offer("5")); // false 超出容量后不再添加入队

    // 3.获取队列首部元素 :3
    System.out.println(queue.peek());

    // 4.移除元素
    System.out.println("移除元素:"+queue.poll());
    System.out.println("移除元素:"+queue.poll());

    // 注意:队列最多加入2个,上述已经全部移除了,此时继续poll会输出null
    System.out.println("移除元素: "+queue.poll()); 

    System.out.println("队列:"+queue);

}

执行结果:1. 队列已满,继续offer添加元素返回false    2. 针对空队列继续poll移除,会返回null

方式三:阻塞等待

/**
  等待,阻塞(一直等待)
  put/take:超出长度无法添加,等待阻塞 / 队列内没有元素,等待阻塞
  ———— 直到添加成功或删除成功,程序才会结束
*/
public static void main(String[] args) throws InterruptedException {
    // 1.定义容量为2的队列
    ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
  
    // 2.添加元素
    queue.put("a");
    queue.put("b");
    //queue.put("c"); // 等待阻塞,直到添加成功,程序才会结束

    // 3.移除元素
    System.out.println("移除元素:"+queue.take());
    System.out.println("移除元素:"+queue.take());

    // 注意:此时队列已为空,继续take则进入等待阻塞,直到队内有元素可删除,程序才结束
    System.out.println("移除元素:"+queue.take()); 

    System.out.println("队列:"+queue);
}

执行结果:

方式四:超时等待

/**
  等待,阻塞(超时等待)
  offer/poll:超出指定时间,还未操作成功,则忽略此次添加/删除
*/
public static void main(String[] args) throws InterruptedException {
    // 1.定义容量为2的队列
    ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);

    // 2.添加元素
    queue.offer("A");
    queue.offer("B");

    // 3.队列已满,使用超时添加:超时2秒后,还未添加成功,则忽略此次offer
    queue.offer("C",2, TimeUnit.SECONDS); 

    // 4.移除元素
    System.out.println("移除元素:"+queue.poll());
    System.out.println("移除元素:"+queue.poll());

    // 同超时offer
    System.out.println("移除元素: "+queue.poll(2, TimeUnit.SECONDS)); 

    System.out.println("队列:"+queue);
}

执行结果:添加第三个元素,等待了2秒后添加失败,则忽略此次添加继续执行后面代码


队列类型

SynchronousQueue 同步队列

特殊的阻塞队列,容量为0,也无法设置容量大小。put了一个元素,必须等待消费者线程take取出来,才能再进行下一个元素的插入操作

类似一个中介,从生产者手中拿到任务直接转交给消费者,直接传递机制,而非存储元素

代码示例:

public static void main(String[] args) {
        // 1.初始化一个同步队列
        BlockingQueue<String> queue = new SynchronousQueue<String>();

        // 2.定义2个线程分别为线程A、线程B,线程A添加元素,线程B取出元素
        new Thread(()->{
            try {
                // 添加3个元素
                System.out.println(Thread.currentThread().getName()+":put 1");
                queue.put("1");

                System.out.println(Thread.currentThread().getName()+":put 2");
                queue.put("2");

                System.out.println(Thread.currentThread().getName()+":put 3");
                queue.put("3");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程A").start();

        new Thread(()->{
            try {
                // 每隔3秒取出一个元素
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+queue.take());

                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+queue.take());

                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+queue.take());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程B").start();

    }

执行结果:put一个元素,须等待消费者线程取出来,下一个元素才能put进去

LinkedBlockingQueue 链表队列

基于链表实现,创建时如不指定大小,默认为Integet.MAX_VALUE,如果生产者的速度远大于消费者速度,则队列内会堆积请求,可能会引起OOM,由于是基于链表模式,每次放入元素会构造一个新节点对象,大量并发下同样会对GC造成一定影响。阻塞方式使用通知模式来实现,生产者和消费者分别使用两把重入锁实现同步,提高系统并发度

ArrayBlockingQueue 数组队列

基于数组实现的阻塞队列,创建对象时必须指定容量大小,且可指定是否公平(默认非公平,即不保证等待时间最长的队列最优先能够访问队列),存取元素的阻塞方法put/take,使用通知模式来实现。生产者和消费者存取数据用的同一把重入锁,无法真正实现生产者和消费者的并行

put/take方法实现的阻塞方式——通知模式(await等待 / signal唤醒),源码如下:

/**  
  添加元素
  译:在此队列的尾部插入指定的元素,如果队列已满,则等待空间变得可用
*/
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 如果队列满了: 队列中的元素数 count = items.length 当前队列条目数
        while (count == items.length)
            notFull.await(); // 注意:队列满了,进入阻塞等待
        // 如果队列没满,则执行添加
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

/**  
  移除元素
*/
public E take() throws InterruptedException {
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();
     try {
          // 如果队列为空,则进入阻塞等待
          while (count == 0)
            notEmpty.await();

          // 如果队列不为空,执行移除元素操作
          return dequeue();
     } finally {
          lock.unlock();
     }
}

/**  
  移除具体操作
  译:提取当前位置、前进和信号的元素。仅在持有锁定时调用。
*/
private E dequeue() {
     // assert lock.getHoldCount() == 1;
     // assert items[takeIndex] != null;
     final Object[] items = this.items;
items index for next take, poll, peek or remove
     // takeIndex:下一次移除的索引
     @SuppressWarnings("unchecked")
     E x = (E) items[takeIndex]; // 通过索引获取当前要被移除的元素
     items[takeIndex] = null; // 重置队列该索引位置 = null 清空元素
     if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
     if (itrs != null)
          itrs.elementDequeued();

     // 注意:上述队列已满put时,进入wait阻塞,这里通知唤醒了该condition
     notFull.signal();  
     return x;
}


ArrayBlockingQueue 和 LinkedBlockingQueue区别:

ArrayBlockingQueueLinkedBlockingQueue
阻塞方式通知模式通知模式
是否有界有界,须指定初始化大小有界,构造器可以不初始化,默认为INTEGER.MAX_VALUE
存取元素是否会创建和销毁不会
生产者消费者锁使用一把锁各自使用不同的锁

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值