文章目录
BlockingQueue
BlockingQueue常常运用于线程池的阻塞队列中,顾名思义,它能够作为一个具有阻塞作用的先进先出队列。
BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的方法用于不同的场景中使用:1、抛出异常;2、返回特殊值(null 或 true/false,取决于具体的操作);3、阻塞等待此操作,直到这个操作成功;4、阻塞等待此操作,直到成功或者超时指定时间。总结如下:
throw exception | special value | blocks | times out | |
---|---|---|---|---|
insert | add(e) | offer(e) | put(e) |
offer(e,time,unit) |
delete | remove() | poll() | take() |
poll(time,unit) |
examine | element() | peek() | not applicable | not applicable |
实现1:ArrayBlockingQueue
ArrayBlockingQueue:基于数组结构的有界阻塞队列,并发控制通过ReentrantLock获得锁来实现。
并发同步原理:读操作和写操作都需要获取到 AQS 独占锁才能进行操作【对比之下LinkedBlockingQueue有两个锁,分别为读取锁和写入锁,能同时读和写】。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。
属性及构造函数
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/**
* 存放元素的数组
*/
final Object[] items;
/**
* 下一次读取操作的位置
*/
int takeIndex;
/**
* 下一次存放操作的位置
*/
int putIndex;
/**
* 队列中对象数量
*/
int count;
//下面三个为控制并发用到的同步器
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();
}
//指定集合的构造函数,将此集合中的元素在构造方法期间就先添加到队列中。
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
}
put
public void put(E e) throws InterruptedException {
//元素不能为null
checkNotNull(e);
final ReentrantLock lock = this.lock;
//可中断锁
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} 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();
}
take
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;
@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;
}
实现2:LinkedBlockingQueue
LinkedBlockingQueue:基于链表结构的“无界”阻塞队列,并发控制同样通过ReentrantLock获得锁来实现,不过LinkedBlockingQueue采用了双锁
:读取锁和写入锁,相比ArrayBlockingQueue能够同时进行读和写,吞吐量更高!
线程池Executors.newFixedThreadPool()采用了此队列。
并发同步原理
使用了两个锁,两个Condition
takeLock和notEmpty 搭配:如果获取(take)一个元素,首先要获得锁(takeLock),这还不够,还要继续判断队列是否为空(notEmpty)这个条件(Condition)
putLock和notFull 搭配:如果插入(put)一个元素,首先要获得锁(putLock),这还不够,还要继续判断队列是否已满(notFull)这个条件(Condition)
属性及构造函数
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//很简单的结点结构,包含了该结点的元素和后继结点
static class Node<E> {
E item;
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;
//元素读取操作时需要获得的锁
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
//元素存入操作时需要获得的锁
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
//因为默认队列长度大小为有符号整数的最大值,所以我们称其为“无界队列”
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);
}
//利用集合初始化的构造函数
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();
}
}
put
public void put(E e) throws InterruptedException {
//元素不能为null
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
//c初始化为-1 ,用于标识插入操作是否成功,offer方法返回布尔值就是通过c是否>=0来判断的
int c = -1;
Node<E> node = new Node<E>(e);
//加锁
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
//队列满了则阻塞
while (count.get() == capacity) {
notFull.await();
}
//新的元素结点插到队列尾部
enqueue(node);
c = count.getAndIncrement();
//c + 1 < capacity说明队列未满,调用 notFull.signal() 唤醒等待线程
if (c + 1 < capacity)
notFull.signal();
} finally {
//入队后释放锁
putLock.unlock();
}
//c==0说明之前队列是空的,这时插入一个元素后队列不为空,因此唤醒notEmpty这个条件
if (c == 0)
signalNotEmpty();
}
//入队很简单,将新结点插入到队列末尾,last指针更新
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
//出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
//头结点一直保持为null,每次从队列中取元素实际上是取头结点后面一个节点的值
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
实现3:SynchronousQueue
SynchronousQueue:不存储元素的阻塞队列,即队列是虚的,不存储元素,吞吐量通常高于LinkedBlockingQueue
,线程池Executors.newCacheThreadPool()采用了此队列。
并发同步原理
当一个线程往队列中写元素时,写入操作不会立即返回,需要等待另一个线程来将这个元素拿走;同理当一个读线程做读操作的时候,同样需要一个相匹配的写线程的写操作。
这里的Synchronous指的就是写线程要和读线程同步,一个写线程匹配一个读线程。
SynchronousQueue 的队列其实是虚的,其不提供任何空间(一个都没有)来存储元素。数据必须从某个写线程交给某个读线程,而不是写到某个队列中等待被消费。
属性及构造函数
SynchronousQueue内部有一个抽象类Transferer,它的两个实现类TransferQueue和TransferStack分别用于公平和非公平模式下。
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//默认非公平模式
public SynchronousQueue() {
this(false);
}
//设定公平模式的构造函数,公平用TransferQueue,非公平用TransferStack
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
//抽象类Transferer中只有一个抽象方法,让我们来看一下:
abstract static class Transferer<E> {
/**
* Performs a put or take.
*
* @param e 如果非null,表示将元素从生产者转移到消费者
* 如果为null,表示消费者等待生产者提供元素,返回值为生产者提供的元素
* @param timed 是否设置超时
* @param 超时时间
* @return 非空,则表示转移的元素;
* 空,则表示超时或者中断。
* 调用者可以通过线程的中断状态判断具体是哪种情况导致的
*/
abstract E transfer(E e, boolean timed, long nanos);
}
}
我们先采用公平模式分析源码,然后再说说公平模式和非公平模式的区别。
put和take
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
//根据transfer的返回值是否为null来判断是否转移元素成功
if (transferer.transfer(e, false, 0) == null) {
//未成功,线程中断,抛出异常
Thread.interrupted();
throw new InterruptedException();
}
}
public E take() throws InterruptedException {
//和put的区别在于第一个参数为null,表示取元素
E e = transferer.transfer(null, false, 0);
//根据transfer的返回值是否为null来判断是否转移元素成功
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
由代码可以看出take和put操作都是通过transfer实现的,它们对此方法实现最大的区别在于方法的第一个参数,put不为null,take为null,可以说SynchronousQueue的核心功能就是通过transfer的实现的,下面来看transfer方法。
transfer
公平模式
transfer的设计思路:
- 调用此方法时,若队列为空或者队列中的结点和当前线程的操作类型一致(即当前操作为put操作,队列中的结点的线程属性都为写线程;take操作和读线程同理),则将当前线程加入到等待队列中
2.如果队列中有等待结点,而且与当前操作匹配(即当前操作位put操作,队列中结点线程线程属性都为读线程,这就构成了匹配;take操作和写线程同理)。这种情况下,匹配等待队列的队头,出队,返回响应数据。
下面来看看官方注释:
/* Basic algorithm is to loop trying to take either of
* two actions:
*
* 1. If queue apparently empty or holding same-mode nodes,
* try to add node to queue of waiters, wait to be
* fulfilled (or cancelled) and return matching item.
*
* 2. If queue apparently contains waiting items, and this
* call is of complementary mode, try to fulfill by CAS'ing
* item field of waiting node and dequeuing it, and then
* returning matching item.
*
* In each case, along the way, check for and try to help
* advance head and tail on behalf of other stalled/slow
* threads.
*
* The loop starts off with a null check guarding against
* seeing uninitialized head or tail values. This never
* happens in current SynchronousQueue, but could if
* callers held non-volatile/final ref to the
* transferer. The check is here anyway because it places
* null checks at top of loop, which is usually faster
* than having them implicitly interspersed.
*/
对于上面提到的队列结点,它的结构是这样的:
/**
* 等待队列的结点类
*/
static final class QNode {
volatile QNode next; // 只有有个后继结点指针,说明是单向链表
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // 保存线程对象,用于挂起和唤醒
final boolean isData; //判断是写线程结点还是读线程结点 boolean isData = (e != null);
}
下面正式分析transfer源码:
//==================TransferQueue的transfer方法===============================
@SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);
for (; ; ) {
QNode t = tail;
QNode h = head;
//头结点和尾结点为空,说明还没有初始化,continue即可。
if (t == null || h == null) // saw uninitialized value
continue; // spin
//队列已初始化,只有一个空结点或者当前结点与队列结点类型一致
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
//说明刚才有结点入队,继续continue即可
if (t != tail) // inconsistent read
continue;
if (tn != null) { // lagging tail
//尾结点的后继结点不为空,说明不是真正的尾结点,CAS将后继结点设为尾结点,继续循环判断
advanceTail(t, tn);
continue;
}
//用于超时设置
if (timed && nanos <= 0) // can't wait
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s)) // failed to link in
continue;
//将新结点s设为新的尾结点
advanceTail(t, s); // swing tail and wait
Object x = awaitFulfill(s, e, timed, nanos);
//当这里,说明之前的线程被唤醒了
//x==s,说明线程等待超时或者被中断,就要取消等待
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 分支就是上面说的第二种情况,有相应的读或写相匹配的情况
else { // complementary-mode
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.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;
}
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) {
/* Same idea as TransferStack.awaitFulfill */
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
//判断需要自旋的次数
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
//循环
for (; ; ) {
//如果被中断了,则取消该结点,就是将s中的item属性设为this
if (w.isInterrupted())
s.tryCancel(e);
Object x = s.item;
//方法的唯一出口,调用tryCancel后(超时或者中断),x!=e
if (x != e)
return x;
//如果需要,检查是否超时
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)
--spins;
//自旋次数已到,先检查s的线程是否为空,为空则设置为当前线程
else if (s.waiter == null)
s.waiter = w;
//自旋次数已到,并且s也封装了当前的线程,则挂起
else if (!timed)
LockSupport.park(this);
//自旋次数已到,当有设置超时时, spinForTimeoutThreshold 这个之前讲 AQS 的时候其实也说过,剩余时间小于这个阈值的时候,就
// 不要进行挂起了,自旋的性能会比较好
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
}
非公平模式
通过分析TransferQueue的transfer源码大概了解了公平模式 下的put和get操作,对于非公平模式,也就是TransferStack的transfer源码实现,直接叙述不加以分析了(参考:https://www.javadoop.com/post/java-concurrent-queue)
- 当调用这个方法时,如果队列是空的,或者队列中的节点和当前的线程操作类型一致(如当前操作是 put 操作,而栈中的元素也都是写线程)。这种情况下,将当前线程加入到等待栈中,等待配对。然后返回相应的元素,或者如果被取消了的话,返回 null。
- 如果栈中有等待节点,而且与当前操作可以匹配(如栈里面都是读操作线程,当前线程是写操作线程,反之亦然)。将当前节点压入栈顶,和栈中的节点进行匹配,然后将这两个节点出栈。配对和出栈的动作其实也不是必须的,因为下面的一条会执行同样的事情。
- 如果栈顶是进行匹配而入栈的节点,帮助其进行匹配并出栈,然后再继续操作。
应该说,TransferStack 的源码要比 TransferQueue 的复杂一些,如果读者感兴趣,请自行进行源码阅读。
两种模式的对比总结
公平模式
- 采用FIFO队列思想,队尾匹配队头出队,先进先出,体现了公平原则;
- 新来的线程若匹配成功,不需要入队,直接唤醒队头线程(注意:head结点是空节点,这里是指head结点的后继结点)
非公平模式:
- 采用栈思想,栈顶元素先匹配,先入栈的线程结点后匹配,体现了非公平原则
- 新来的线程若匹配成功,则需要压栈,然后新线程循环执行匹配线程逻辑,一旦发现没有并发冲突则成对出栈
这篇文章 https://zhuanlan.zhihu.com/p/29227508 用图来表示两种模式的put和take过程,清晰易懂
实现4:PriorityBlockingQueue
PriorityBlockingQueue:一个具有优先级的无界阻塞队列。优先级是因为采用堆思想,可以根据元素自身排序,也可以指定比较器进行排序;无界是因为队列能够自动扩容。
并发同步原理
当一个线程往队列中写元素或者读取元素时,会获取独占锁(ReentrantLock),和ArrayBlocking的同步实现原理很像,区别在于PriorityBlockingQueue中没有notFull这个条件(Condition)
属性及构造函数
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//默认初始队列容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//队列最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//队列采用数组存储元素,思想是堆
private transient Object[] queue;
//队列中元素个数
private transient int size;
//比较器
private transient Comparator<? super E> comparator;
//独占锁
private final ReentrantLock lock;
//条件Condition
private final Condition notEmpty;
//扩容时采用的锁,通过CAS实现
private transient volatile int allocationSpinLock;
//默认构造器,初始化队列大小为11
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
//指定队列大小的构造器
public PriorityBlockingQueue(int initialCapacity) {
this(initialCapacity, null);
}
//指定队列大小和比较器的队列
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
}
put
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
//1 获取独占锁
lock.lock();
int n, cap;
Object[] array;
//2. 插入元素前判断是否需要扩容
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
//3. 插入元素并调整堆结构
try {
Comparator<? super E> cmp = comparator;
//构造器为空,使用插入元素自带比较器进行调整
if (cmp == null)
siftUpComparable(n, e, array);
//否则使用指定的构造器进行调整
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
//4. 通知notEmpty条件
notEmpty.signal();
} finally {
//5. 释放锁
lock.unlock();
}
return true;
}
由于在插入元素前会判断是够需要扩容,下面来讲一下如何扩容
tryGrow扩容
/**
* 扩容过程中释放了独占锁,通过CAS 操作维护 allocationSpinLock状态,实现专门的扩容锁, 这样就可以保证扩容操作和读操作同时进行
* @param array
* @param oldCap
*/
private void tryGrow(Object[] array, int oldCap) {
//释放独占锁
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// 用 CAS 操作将 allocationSpinLock 由 0 变为 1,算是获取扩容锁
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
//queue如果不等于array,说明有其他线程给queue分配了空间,newArray为null
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
//释放扩容锁
allocationSpinLock = 0;
}
}
//等待其他线程操作完毕
if (newArray == null) // back off if another thread is allocating
Thread.yield();
//重新获得独占锁
lock.lock();
// 将原来数组中的元素复制到新分配的大数组中
if (newArray != null && queue == array) {
queue = newArray;
//将旧队列中元素复制到新队列中
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
take
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//获取独占锁
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
//释放独占锁
lock.unlock();
}
return result;
}
//元素出队列,并调整堆结构
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
总体来说,PriorityBlockingQueue 也是比较简单的,内部用数组实现堆结构,通过一个独占锁来控制put和take的线程安全性,通过CAS操作改变allocationSpinLockOffset状态来达到获取扩容锁的目的,这样扩容操作和读操作可以同时进行,提高吞吐量。
总结
ArrayBlockingQueue :基于数组结构的有界阻塞队列,并发控制通过一个ReentrantLock和两个Condition实现,使用生产者-消费者模式的很好选择。
LinkedBlockingQueue :基于链表结构的无界阻塞队列,可以设置初始队列容量使其有界,并发控制通过两个个ReentrantLock和两个Condition实现。
SynchronousQueue :本身不带有空间来存储任何元素,分为公平模式和非公平模式两种,公平模式通过FIFO队列思想来实现,非公平模式通过栈思想来实现。
PriorityBlockingQueue :带有优先级的无界阻塞队列,基于数组实现堆结构,能够自动扩容。