BlockingQueue - 阻塞队列(一)

本文介绍阻塞队列基本原理以及两种基本的阻塞队列 ArrayBlockingQueue & LinkedBlockingQueue

​ 阻塞队列是线程池中的一个重要角色,JDK 原生实现的阻塞队列有四种 LinkedBlockingQueueSynchronousQueueArrayBlockingQueue 以及 PriorityBlockingQueue

​ 阻塞队列与其他队列显著的区别有两点:

  1. 既然用于线程池,必须要支持并发(线程安全)

  2. 提供阻塞添加与阻塞删除方法

    在传统队列的基础上,要提供 阻塞 的数据操作接口。

    阻塞添加 调用了该方法之后,如果队列为,调用线程会被阻塞。直到队列取出了一个元素时才会唤醒这些线程。

    阻塞删除 调用了该方法之后,如果队列为,调用线程会被阻塞。直到队列加入了一个元素时才会唤醒这些线程。

    注:这里的 唤醒 使用 ReentrantLock 中的 Condition 实现,并非 JDK 原生的 wait & notify

jdk api 中对阻塞队列解释如下:

BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:

抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()不可用不可用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AyPP9haK-1606213736287)(…/img/阻塞队列.png)]

图 1. 阻塞队列类图

ArrayBlockingQueue

ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据(实际上可看作一个循环数组)。常用的操作包括 add,offer,put,remove,poll,take,peek。

根据 ArrayBlockingQueue 的名字我们都可以看出,它是一个队列,并且是一个基于数组的阻塞队列

ArrayBlockingQueue 是一个有界队列,有界也就意味着,它不能够存储无限多数量的对象。所以在创建 ArrayBlockingQueue 时,必须要给它指定一个队列的大小。

我们先来熟悉一下 ArrayBlockingQueue 中的几个重要的方法。

  • add(E e):把 e 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则报IllegalStateException 异常
  • 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 特性

  • 先进先出队列(队列头的是最先进队的元素;队列尾的是最后进队的元素)
  • 有界队列(即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞进队操作;容量空,则阻塞出队操作
  • 队列不支持 Null 元素

ArrayBlockingQueue 部分源码

/** The queued items */
final Object[] items;
/** 用数组实现队列,需要有两个 Index 【注意这个队列实际是循环队列】 */
int takeIndex;
int putIndex;
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;

/*
    1. 可以看到,ArrayBlockingQueue 底层使用数组存放数据
    2. 使用 ReentrantLock 全局锁
    3. 使用 notEmpty & notFull 两个条件信号 唤醒被阻塞的线程
    4. Condition 的方法:
    		notFull.await(); // 将当前调用线程挂起,添加到notFull条件队列中等待唤醒
    		notEmpty.signal(); // 唤醒 在notEmpty阻塞队列中 的线程
*/

注视也说明了,这是一个掌管所有访问操作的锁。全局共享。都会使用这个锁。

ArrayBlockingQueue 进队操作采用了**加锁(ReentrantLock)**的方式保证并发安全。源代码里面有一个 while() 判断:

/**
 * 阻塞添加 put
 */
public void put(E e) throws InterruptedException {
  checkNotNull(e); // 非空判断
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly(); // 获取锁 - 可被打断
  try {
    while (count == items.length){
      // 调用 await 即阻塞,被唤醒后还会判断是否满,如果满了,继续调用 await 阻塞
      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();
  }
}

//入队操作
private void enqueue(E x) {
    //获取当前数组
    final Object[] items = this.items;
    //通过putIndex索引对数组进行赋值
    items[putIndex] = x;
    //索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;//队列中元素数量加1
    //唤醒调用take()方法的线程,执行元素获取操作。
    notEmpty.signal();
}

通过源码分析,我们可以发现下面的规律:

  • 阻塞调用方式 put(e)offer(e, timeout, unit)
  • 阻塞调用时,唤醒条件为超时或者队列非满(因此,要求在出队时,要发起一个唤醒操作)
  • 进队成功之后,执行 notEmpty.signal() 唤起被阻塞的出队线程

出队的源码类似。ArrayBlockingQueue 队列我们可以在创建线程池时进行使用。

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(2));

PS:在进行某项业务存储操作时,建议采用offer进行添加,可及时获取boolean进行判断,如用put要考虑阻塞情况(队列的出队操作慢于进队操作),资源占用。

公平锁与非公平锁

ArrayBlockingQueue 中的元素存在公平访问与非公平访问的区别 (实际是 ReentrantLock 提供的能力),对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。

// 默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
// 公平阻塞队列
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(2,true);

// 构造方法源码
public ArrayBlockingQueue(int capacity) {
     this(capacity, false);
 }

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

LinkedBlockingQueue

LinkedBlockingQueue 是由 链表 实现的 阻塞队列,值得注意的是,Java 中并没有定义链表接口数据节点类型的数据结构,所以每个链表都需要自己实现一个内部的数据类 Node。另外,LinkedBlockingQueue 中默认最大长度是 Integer.MAX_VALUE ,所以我们在使用 LinkedBlockingQueue 时建议手动传值,为其提供我们所需的大小,避免队列过大造成机器负载或者内存爆满等情况。

//默认大小为Integer.MAX_VALUE
public LinkedBlockingQueue() {
       this(Integer.MAX_VALUE);
}

//创建指定大小为capacity的阻塞队列
public LinkedBlockingQueue(int capacity) {
     if (capacity <= 0) throw new IllegalArgumentException();
     this.capacity = capacity;
     last = head = new Node<E>(null);
 }

//创建大小默认值为Integer.MAX_VALUE的阻塞队列并添加c中的元素到阻塞队列
public LinkedBlockingQueue(Collection<? extends E> c) {
     this(Integer.MAX_VALUE);
     final ReentrantLock putLock = this.putLock;
     putLock.lock(); // Never contended, but necessary for visibility
     try {
         int n = 0;
         for (E e : c) {
             if (e == null)
                 throw new NullPointerException();
             if (n == capacity)
                 throw new IllegalStateException("Queue full");
             enqueue(new Node<E>(e));
             ++n;
         }
         count.set(n);
     } finally {
         putLock.unlock();
     }
 }

​ 在正常情况下,链表队列的吞吐量要高于基于数组的队列ArrayBlockingQueue),因为其内部实现添加和删除操作使用的两个ReenterLock来控制并发执行,而 ArrayBlockingQueue内部只是使用一个ReenterLock控制并发。注意LinkedBlockingQueueArrayBlockingQueueAPI几乎是一样的,但它们的内部实现原理不太相同。

/**
 * 节点类,用于存储数据
 */
static class Node<E> {
  E item;
	/**
	 * One of:
   * - the real successor Node
   * - this Node, meaning the successor is head.next
   * - null, meaning there is no successor (this is the last node)
   */
	Node<E> next;
	Node(E x) { item = x; }
}

​ 这里的链表,实际上是一个单向链表,(注意 LinkedList 中是一个双向链表)

主要参数:

    /** 阻塞队列的大小,默认为Integer.MAX_VALUE */
    private final int capacity;

    /** 当前阻塞队列中的元素个数 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 阻塞队列的头结点
     */
    transient Node<E> head;

    /**
     * 阻塞队列的尾节点
     */
    private transient Node<E> last;

    /** 获取并移除元素时使用的锁,如take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 添加元素时使用的锁如 put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** notFull条件对象,当队列数据已满时用于挂起执行添加的线程 */
    private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue和ArrayBlockingQueue对比

ArrayBlockingQueueLinkedBlockingQueue
大小有界数组,必须初始化时指定大小可指定有界,默认无界
存储容器数组单向链表
额外对象Node (可能对 GC 有压力)
锁策略添加删除使用同一把锁添加删除使用不同的锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值