一、BlockingQueue 简介
BlockingQueue 接口继承了 Queue,它是一个先进先出的队列(Queue),为什么说是阻塞(Blocking)的呢?是因为 BlockingQueue 支持当获取队列元素但是队列为空时,会阻塞等待队列中有元素再返回;也支持添加元素时,如果队列已满,那么等到队列可以放入新元素时再放入。
BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的模式,用于应用不同的场景中:1、抛出异常;2、返回特殊值(null 或 true/false,取决于具体的操作);3、阻塞等待此操作,直到这个操作成功;4、阻塞等待此操作,直到成功或者超时指定时间。总结如下:
Throws exception | Special value | Blocks | Times out | |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | not applicable | not applicable |
为什么会有这么多不同的模式?因为 Queue 这个接口继承了 Collection ,add、remove 来自 Collection 操作集合的基本操作,它规定了队列容量满了或者容量为 0,会有抛出异常的情况;offer、poll、peek 来自 Queue 操作队列的基本操作,它规定了队列容量满了或者容量为 0,会返回 boolean 值或者 null;put、take 操作来自 BlockingQueue 操作队列的基本操作,它规定了队列容量满了或者容量为 0,会把当前线程阻塞。所以学习 BlockingQueue 我们重点还是要关注 put、take 这两个操作。
BlockingQueue 有一下几个特点:
- BlockingQueue 不接受 null 值的插入,插入 null 时会抛出 NullPointerException 异常。null 值在这里通常用于作为特殊值返回(表格中的第三列),代表 poll 失败。
- BlockingQueue 并不是绝对无界的,容量最多是 Integer.MAX_VALUE,所以通常说是无界的。
- BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll, retainAll 和 removeAll 不一定是原子操作。如 addAll© 有可能在添加了一些元素后中途抛出异常,此时 BlockingQueue 中已经添加了部分元素,这个是允许的,取决于具体的实现。
二、ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。它的构造函数中,初始化的容量大小是必须指定的,其他可选:是否使用公平锁、指定集合初始化后添加进数组中。
ArrayBlockingQueue 的几个基本属性:
//存放元素的数组
final Object[] items;
//下次读取操作的位置
int takeIndex;
//下次写入操作的位置
int putIndex;
//已有元素的个数
int count;
//并发控制的锁
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
看到 ReentrantLock 我们就应该明白 ArrayBlockingQueue 的同步原理,前面讲 Condition 的使用时就是举的这个例子,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。队列为空,线程进入到读线程队列排队;队列已满,线程进入到写线程队列排队。在新加入元素或者取出元素后才唤醒阻塞的队列。
三、LinkedBlockingQueue
注意:jdk7 和 jdk8 的实现方式上有些许不同。下面只讲 jdk8 的 LinkedBlockingQueue
LinkedBlockingQueue 底层采用链表的方式实现的有界或者无界队列,构造函数中可以指定初始值也可以不指定,内部使用 Node 来实现链表的方式。jdk7 是单向链表,jdk8 是双向链表实现,其实就是有前指针和后指针了。
LinkedBlockingQueue 的几个基本属性:
//队头
transient Node<E> first;
//队尾
transient Node<E> last;
//已有元素的个数
private transient int count;
//队列容量
private final int capacity;
//并发控制的锁
final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
jdk7 是使用两个 ReentrantLock 和两个 Condition来实现的,比 jdk8 的实现会复杂一点。
简单的看一下 put 方法:
public void putFirst(E e) throws InterruptedException {
//不允许添加null值
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
while (!linkFirst(node))
notFull.await();
} finally {
lock.unlock();
}
}
private boolean linkFirst(Node<E> node) {
//元素个数超过设置容量
if (count >= capacity)
return false;
Node<E> f = first;
node.next = f;
first = node;
if (last == null)
last = node;
else
f.prev = node;
++count;
notEmpty.signal();
return true;
}
仔细看一下,offer、put、add 其实都是调用 link 方法的,poll、take、remove 调用的是 unlink 方法。jdk7 是没有 First 和 Last 操作的,属于 jdk8 拓展了链表的使用。而进入到 link 方法前已经获取独占锁了,可以保证线程安全,所以内部操作直接进行修改值操作就可以了。
了解了 ReentrantLock 原理,上面的代码还是很简单的,主要在 link() 方法中检查当前元素个数 count 是否达到设置容量 capacity 或者是否是 0,返回 false,调用 await 阻塞线程,等待唤醒;返回 true,直接操作。
四、SynchronousQueue
SynchronousQueue 是一种特殊的队列,它不像前两种队列中内部有存储元素的数组或者链表,SynchronousQueue 内部不提供存储元素的元器。它的名字就蕴含了它的特征:同步队列,这里同步的意义不是说多线程的并发,而是说当进行写操作时,不会立即返回,需要等待另一条读操作线程进来时,才会返回,反之亦然。
SynchronousQueue 不存储元素,但是它的内部会使用类似于等待队列的 Transferer 来管理读写操作的线程。当多条操作类型相同的线程进来时,Transferer 会使用单向链表 SNode 来存储线程,将线程一一挂起,等待操作类型匹配的线程进来唤醒。
下面看一下 SynchronousQueue 的构造函数和读写操作:
//SynchronousQueue构造函数
public SynchronousQueue() {
this(false);
}
//TransferQueue转移队列,意味公平的原则;TransferStack转移堆栈,意味着非公平的原则
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
//写操作
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
//读操作
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
SynchronousQueue 完成相应读写操作是靠 Transferer 的 transfer 方法实现的,不同类型的操作主要看第一个参数是否是 null。 Transferer 共有两个实现类,其实就是公平与非公平的区别,下面看 TransferQueue 的 transfer 方法:
E transfer(E e, boolean timed, long nanos) {
//文档翻译:根据需要重建或者重构
QNode s = null;
//e为null是读线程,e不为null是写线程
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // saw uninitialized value
continue; // spin
//队列为空或者新加入元素的读写类型与队列中的一致,也就说需要挂起线程
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
//有节点刚刚入队,再循环一次,只拿等待队列的尾节点
if (t != tail) // inconsistent read
continue;
//有节点入队,但是还没来及修改tail,重新设置tail即可
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0) // can't wait
return null;
//将新传入的e包装成QNode
if (s == null)
s = new QNode(e, isData);
//将新生成的SNode,插入到tail的下一个节点中,失败的话代表有其他几点刚刚插入
if (!t.casNext(null, s)) // failed to link in
continue;
//更新tail
advanceTail(t, s); // swing tail and wait
//挂起后又被唤醒
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;
//说明新加入元素的读写类型正好匹配,直接写入元素或者读出元素即可
} else { // complementary-mode
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
//正常的话会修改x的item值,用于唤醒后退出循环使用
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}
//将head设置为等待队列第一个节点
advanceHead(h, m); // successfully fulfilled
//唤醒这个节点的线程
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(e);
Object x = s.item;
//这是唯一的出口,判断条件就是item是否还是e
if (x != e)
return x;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)
--spins;
else if (s.waiter == null)
s.waiter = w;
//将线程挂起等待唤醒
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
不一定要把所有方法都看懂,看出来怎么挂起,怎么退出循环即可,所有的读写操作都在这个方法中完成,并且有唤醒等待线程的操作,很简洁,但是不好读。TransferStack 代码会稍复杂一点,原理也差不多。
SynchronousQueue 记住最重要的一点就可以了,它本身不会存储任何元素,所以不能使用 peek 方法,也不可以拿来当普通的 Collection 来用。