Java线程池队列ArrayBlockingQueue和LinkedBlockingQueue

222 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

public enum QueueTypeEnum {
    ARRAY_BLOCKING_QUEUE(1, "ArrayBlockingQueue"),
    LINKED_BLOCKING_QUEUE(2, "LinkedBlockingQueue"),
    PRIORITY_BLOCKING_QUEUE(3, "PriorityBlockingQueue"),
    DELAY_QUEUE(4, "DelayQueue"),
    SYNCHRONOUS_QUEUE(5, "SynchronousQueue"),
    LINKED_TRANSFER_QUEUE(6, "LinkedTransferQueue"),
    LINKED_BLOCKING_DEQUE(7, "LinkedBlockingDeque"),
    VARIABLE_LINKED_BLOCKING_QUEUE(8, "VariableLinkedBlockingQueue"),
    MEMORY_SAFE_LINKED_BLOCKING_QUEUE(9, "MemorySafeLinkedBlockingQueue");
}

ArrayBlockingQueue和LinkedBlockingQueue

1、ArrayBlockingQueue是一个阻塞式的队列继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列,实际上可看作一个循环数组),其内部按先进先出的原则对元素进行排序。常用的操作包括 add,offer,put,remove,poll,take,peek。其中put方法和take方法为添加和删除的阻塞方法。

2、LinkedBlockingQueue是底层基于链表实现的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。内部维持着一个数据缓冲队列(该队列由链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

LinkedBlockingQueue可以指定容量,内部维持一个队列,所以有一个头节点head和一个尾节点last,内部维持两把锁,一个用于入队,一个用于出队,还有锁关联的Condition对象。

//容量,如果没有指定,该值为Integer.MAX_VALUE;
private final int capacity;
//当前队列中的元素
private final AtomicInteger count = new AtomicInteger();
//队列头节点,始终满足head.item==null
transient Node<E> head;
//队列的尾节点,始终满足last.next==null
private transient Node<E> last;
//用于出队的锁
private final ReentrantLock takeLock = new ReentrantLock();
//当队列为空时,保存执行出队的线程
private final Condition notEmpty = takeLock.newCondition();
//用于入队的锁
private final ReentrantLock putLock = new ReentrantLock();
//当队列满时,保存执行入队的线程
private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue的构造方法有三个:

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);//last和head在队列为空时都存在,所以队列中至少有一个节点
}

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

LinkedBlockingQueue用一个链表保存元素,其内部有一个Node的内部类,其中有一个成员变量 Node next,这样就形成了一个链表的结构,要获取下一个元素,只要调用next就可以了。而ArrayBlockingQueue则基于数组来保存元素。

LinkedBlockingQueue内部读写(插入获取)各有一个锁,而ArrayBlockingQueue则读写共享一个锁

BlockingQueue 常见方法

BlockingQueue方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:

  • 第一种是抛出一个异常,
    
  • 第二种是返回一个特殊值(null 或 false,具体取决于操作),
    
  • 第三种是在操作可以成功前,无限期地阻塞当前线程,
    
  • 第四种是在放弃前只在给定的最大时间限制内阻塞。
    

image.png

插入

  • add(e): 和collection的add一样,没什么可以说的。如果当前没有可用的空间,则抛出 IllegalStateException
  • offer(e): 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false,不会抛异常;offer(e,timeout, tmeunit)可以指定timeout和timeUnit,等待timeout后返回。
  • put(e):将指定元素插入此队列中,将等待可用的空间.通俗点说就是>maxSize 时候,阻塞,直到能够有空间插入元素

add、offer、put这3个方法都是往队列尾部添加元素,区别如下:

add:不会阻塞,添加成功时返回true,不响应中断,当队列已满导致添加失败时抛出IllegalStateException。

offer:不会阻塞,添加成功时返回true,因队列已满导致添加失败时返回false,不响应中断。

put:会阻塞会响应中断。

消费

  • take(): 获取并移除此队列的头部,在元素变得可用之前一直等待阻。queue的长度 == 0 的时候,一直阻塞。
  • remove(Object o) :移除指定元素,成功返回true,失败返回false。
  • poll(): 获取并移除此队列的头元素,若队列为空,则返回 null;支持time,unit方法参数;poll会响应中断,会阻塞,阻塞时间参照方法里参数timeout.timeUnit,当阻塞时间到了还没取得元素会返回null。

检查方法

  • element():获取但不移除此队列的头元素,没有元素则抛异常。
  • peek():获取但不移除此队列的头;若队列为空,则返回 null。