BlockingQueue是一个阻塞队列的接口,提供了一系列的接口方法。其中方法主要可以分为三类,包括Insert相关的add、offer、put,remove相关的remove()、poll()、take()方法,以及查看相关的peek()、element方法等。阻塞队列是线程安全的容器,其元素不允许为null,否则会抛出空指针异常。阻塞队列可以用于生产者消费者场景中去。
常见的实现类有ArrayBlockingQueue以及LinkedBlockingQueue两种,下面来详细来看一下ArrayBlockingQueue以及LinkedBlockingQueue的具体实现。
ArrayBlockingQueue重要参数及构造函数
/** 队列中元素的数据结构 */
final Object[] items;
/** 队首标记,用于出队操作 */
int takeIndex;
/** 队尾标记,用于入队操作 */
int putIndex;
/** 队列中元素的个数 */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/**对于任何操作都需要加锁,这里是一个可重入锁 */
final ReentrantLock lock;
/** 用于通知取元素的条件 */
private final Condition notEmpty;
/** 控制放元素的条件 */
private final Condition notFull;
/**
* 创造一个给定容量的非公平锁的阻塞队列
*/
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();
}
ArrayBlockingQueue入队操作
以常用的add方法为例子,在这个方法内部实际上是调用了offer方法,而offer方法在加锁的基础之上调用了enqueue方法来将元素放在数组的尾部,并唤醒那些阻塞了的取元素方法。
add、offer、put的主要区别如下:
add方法在成功是返回true,如果失败就抛出异常。
offer方法在添加成功返回true,添加失败返回false。
put方法如果添加不成功就会一直等待。不会出现丢节点的情况一般。
/**
* 将元素插入队列中,如果成功则返回true,否则的话抛出异常。可以看出其本身还是调用了offer方法。
*
* <p>This implementation returns <tt>true</tt> if <tt>offer</tt> succeeds,
* else throws an <tt>IllegalStateException</tt>.
*
*
*/
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full. This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);//判断是否为空,如果元素为null,则抛出异常
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
/**
* 将元素插入数组尾部,并唤醒等待取元素的线程。
*/
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
/**
*向数组中插入元素,如果没有空间就等待。
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
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();
}
}
ArrayBlockingQueue出队操作
出队操作也是要加锁的,以remove为例,从头开始遍历,一直到尾部标记的地方为止,当找到一个和所给元素相等的元素时,删除这个节点的元素。至于take方法和poll方法都是删除头部元素,区别在于take方法会等待,而poll方法如果没有元素可取则会直接返回null。
/**
* Removes a single instance of the specified element from this queue,
* if it is present. More formally, removes an element {@code e} such
* that {@code o.equals(e)}, if this queue contains one or more such
* elements.
* Returns {@code true} if this queue contained the specified element
* (or equivalently, if this queue changed as a result of the call).
*
* <p>Removal of interior elements in circular array based queues
* is an intrinsically slow and disruptive operation, so should
* be undertaken only in exceptional circumstances, ideally
* only when the queue is known not to be accessible by other
* threads.
*
* @param o element to be removed from this queue, if present
* @return {@code true} if this queue changed as a result of the call
*/
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)//当元素为0时,会自动阻塞到条件队列中去。知道被其他方法唤醒。
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Extracts element at current take position, advances, and signals.
* Call only when holding lock.
*/
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
ArrayBlockingQueue总结
ArrayBlockingQueue本质上就是对一个数组进行操作,它严格不允许元素为null,但是这些操作都是加了锁的操作,因此它们都是线程安全的,并且可以根据实际情况来选择锁的公平非公平,公平锁可以严格保证线程不会饥饿,而非公平锁则可以提高系统的吞吐量。并且由于它还是一个队列,对于队列的操作也大多数都是在头部或者是尾部的操作。除了锁之外,ArrayBlockingQueue还提供了两个condition来实现等待操作,这里的方法其实就是把那些被阻塞的线程放在Condition队列中,然后当有signal操作是就唤醒最前面的线程执行。整体而言这些操作就是在数组的基础之上加锁,相对简单。
LinkedBlockingQueue源码分析
类似与ArrayList和LinkedList,和ArrayBlockingQueue对应的是LinkedBlockingqueue,不同的地方在于一个底层是数组实现,一个底层是当链表来实现。再就是一个底层只有一把锁,而另一个有两把锁。首先来看一下其中的重要参数。
LinkedBlockingQueue有两把锁,对应着队尾和队头的操作,也就是说添加和删除操作不互斥,这和上面的是不一样的,因此其并发量要高于ArrayBlockingQueue。
/**
* 这里的节点相对简单,单向链表。
*/
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; }
}
/** 记录链表容量 */
private final int capacity;
/**元素数目 */
private final AtomicInteger count = new AtomicInteger();
/**
* 头结点
*/
transient Node<E> head;
/**
* 尾节点
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
LinkedBlockingQueue入队操作
以offer方法为例子,当我们尝试入队一个null时就会抛出异常。其他情况下当容量不够时就返回false,否则就可以给这个入队操作进行加锁,当元素个数小于容量时就添加节点到尾部然后给count+1,唤醒其他被阻塞的添加元素线程。这里获取的count是还没有加一之前的值,因此它的值如果为0,那么至少队列中还是有一个元素的,可以唤醒消费线程消费了。
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full.
* When using a capacity-restricted queue, this method is generally
* preferable to method {@link BlockingQueue#add add}, which can fail to
* insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();//为null抛出异常
final AtomicInteger count = this.count;
if (count.get() == capacity) // 量不足返回false.
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();//获取put锁
try {
if (count.get() < capacity) {//count小于容量在队尾入队
enqueue(node);
c = count.getAndIncrement();//count加一
if (c + 1 < capacity)
notFull.signal();//仍然有剩余容量,唤醒等待的put线程
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();//唤醒消费线程
return c >= 0;
}
/**
* 这个方法意味这last.next=node,也就是把node作为last的下一个节点。
*然后将last=last.next,也就是把last向后移。
* @param node the node
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
/**
* Signals a waiting take. Called only from put/offer (which do not
* otherwise ordinarily lock takeLock.)
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
LinkedBlockingQueue出队操作
先来看一下remove方法的出队操作。由于remove方法要删除的元素不固定,可能出现在队列中的任何地方,因此需要同时锁住队首和队尾两把锁,然后从头到尾诸葛遍历元素,当找到元素之后就删除这个元素。这里其实类似于单链表中的节点删除,不同的地方在于要加锁!
/**
* Removes a single instance of the specified element from this queue,
* if it is present. More formally, removes an element {@code e} such
* that {@code o.equals(e)}, if this queue contains one or more such
* elements.
* Returns {@code true} if this queue contained the specified element
* (or equivalently, if this queue changed as a result of the call).
*
* @param o element to be removed from this queue, if present
* @return {@code true} if this queue changed as a result of the call
*/
public boolean remove(Object o) {
if (o == null) return false;
fullyLock();
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
poll 方法返回队头元素并删除之。如果队列中没有数据就返回null,否则的话就说明有元素,那么就可以讲一个元素出列,同时将count值减一,C>1意味着队列中至少有一个元素,因此可以唤醒等待着的消费线程进行消费。当c的值等于容量时,此时c的实际值是容量减一,可以唤醒等待添加元素的线程进行添加。因为他们之前最有可能会被阻塞。
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();//获得count值并将其减一
if (c > 1)
notEmpty.signal();//至少有一个元素,唤醒等待着的消费线程。
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();唤醒
return x;
}
LinkedBlockingQueue总结
LinkedBlockingQueue本质上是一个链表,区别于普通链表的地方在于它在队首和队尾的地方加了两把锁,分别对应于生产元素和消费元素,因此和独占锁比起来会快一些(毕竟可以首尾同时操作),它本身的元素也是不允许为null的。
ArrayBlockingQueue和LinkedBlockingQueue比较
1.ABQ的底层是数组实现,LBQ的底层是链表实现。
2.ABQ加锁只加一把,并且是全局的,而LBQ的锁有两把,分别对应着队首和队尾,同时也有两个Condition队列。也就是说,ABQ的取元素和放元素是互斥的,而LBQ则没有相互关联,因此就并发性而言,LBQ要优于ABQ。
3.ABQ 的容量是必须需要的,并且不可以扩容,而LBQ的大小可以指定,也可以不指定,不指定的情况下其值为最大整数值。
4.ABQ 支持公平锁和非公平锁,而LBQ不可指定,其本身内部使用的也是非公平锁。
参考资料:
https://blog.csdn.net/u014082714/article/details/52215130
https://blog.csdn.net/u010412719/article/details/52337471
https://blog.csdn.net/u010887744/article/details/73010691