前言
这篇文章介绍并发包内不同的Queue实现方法,Queue是常用的数据结构之一,并发包对Queue进行了不同的实现。
正文
ArrayBlockingQueue
ArrayBlockingQueue是用数组实现的同步队列,构造时指定数组大小,以循环的方式写入和取出,过程中不涉及到底层数组的变化扩容,看一下属性列表:
final Object[] items;
int takeIndex;
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
以两个指针来表示写入和添加索引,用锁和条件变量来替代synchronized和线程的阻塞唤醒,那么实现方法就很容易就写出来了
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();
}
}
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
看一下put、offer和add方法,都是使用了ReentrantLock加锁,然后判断队列是否已满,区别就在于put是一个阻塞方法,当队列满的时候被条件变量阻塞自旋,而offer和add不进行阻塞,add调用了offer,当失败的时候抛出异常。
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
实际的添加节点逻辑,将putIndex自增且到了尾部则重新回到头部,并且通知因为队列长度为0而获取不到被阻塞的线程。
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() {
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;
}
take方法正好反过来,当队列为空的时候阻塞,同时对应的poll方法则是非阻塞,当获取不到返回null,实现原理简单就不放上来了。
++ArrayBlockingQueue利用了固定长度数组作为存储,ReentrantLock作为锁实现起来很简单,由于是同步锁实现,不太适用于高并发场景。++
LinkedBlockingQueue
LinkedBlockingQueue使用的链表结构,同样也是ReentrantLock来作为同步器
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();
Node内部没有复杂的关系,只保留了实际元素和下一个node,下面只看阻塞的方法,非阻塞方法实现和阻塞差不多。
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
这是一个构造方法,用于指定队列长度,从这里可以看出head节点初始化后是一个空节点
public void put(E e) throws InterruptedException {
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();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
逻辑很简单,主要看看enqueue方法
private void enqueue(Node<E> node) {
last = last.next = node;
}
直接在last后添加一个节点然后把自己成为last....put方法和传统queue无异,在边界(0和最大)处进行了判断去通知条件变量
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;
}
take主要看看dequeue方法,其他和put只是反过来
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;
}
从这里可以看出来,take方法删除了head节点,但是返回的却是head.next的值,结合构造方法初始化head为空,可以了解LinkedBlockingQueue使用两把锁的原因:使用了一个虚拟节点来延迟删除实际的数据节点,好处是put和take方法可以同时进行。
++LinkedBlockingQueue实现了读写分离,提高了效率,但是需要注意的是默认容量是Integer.MAX_VALUE,需要自行调整不然会造成内存飙升++
LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的无界阻塞队列。于上文介绍的阻塞队列比较,LinkedTransferQueue多了tryTransfer和transfer方法。
LinkedTransferQueue使用了预占模式,当一个线程想要获取数据的时候,如果没有可以获取的节点就用一个null节点来添加队列的尾部并阻塞自己,如果有一个线程添加节点,发现了尾节点是一个空节点就将值设置进去并唤醒线程。
static final class Node {
//消费节点为false
final boolean isData;
volatile Object item;
volatile Node next;
//消费者线程
volatile Thread waiter;
//使用Unsafe CAS的方式来替换属性
final boolean casNext(Node cmp, Node val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
final boolean casItem(Object cmp, Object val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
Node(Object item, boolean isData) {
UNSAFE.putObject(this, itemOffset, item); // relaxed write
this.isData = isData;
}
//将next指向自己
final void forgetNext() {
UNSAFE.putObject(this, nextOffset, this);
}
//将线程设置为空和next指向自己,帮助GC
final void forgetContents() {
UNSAFE.putObject(this, itemOffset, this);
UNSAFE.putObject(this, waiterOffset, null);
}
//节点是否被匹配过了
final boolean isMatched() {
Object x = item;
return (x == this) || ((x == null) == isData);
}
//节点是否被匹配过
final boolean isUnmatchedRequest() {
return !isData && item == null;
}
// 如果给定节点不能连接在当前节点后则返回true
final boolean cannotPrecede(boolean haveData) {
boolean d = isData;
Object x;
return d != haveData && (x = item) != this && (x != null) == d;
}
//尝试匹配一个有数据的节点
final boolean tryMatchData() {
// assert isData;
Object x = item;
if (x != null && x != this && casItem(x, null)) {
LockSupport.unpark(waiter);
return true;
}
return false;
}
}
几个重要的参数
// 作为第一个等待节点在阻塞之前的自旋次数
private static final int FRONT_SPINS = 1 << 7;
// 前驱节点正在处理,当前节点在阻塞之前的自旋次数
private static final int CHAINED_SPINS = FRONT_SPINS >>> 1;
// sweepVotes的阈值
static final int SWEEP_THRESHOLD = 32;
// 断开被删除节点失败的次数
private transient volatile int sweepVotes;
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
put方法调用了xfer,true表示这是数据节点
private E xfer(E e, boolean haveData, int how, long nanos) {
//首先判断数据节点的数据不能为空
if (haveData && (e == null))
throw new NullPointerException();
Node s = null;
retry:
for (;;) {
//从head开始循环找未数据节点
for (Node h = head, p = h; p != null;) {
boolean isData = p.isData;
Object item = p.item;
if (item != p && (item != null) == isData) {
//是一个数据节点跳过
if (isData == haveData)
break;
//尝试设置数据
if (p.casItem(item, e)) {
//标注1
for (Node q = p; q != h;) {
Node n = q.next;
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
}
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break;
}
//设置完数据后唤醒这个线程进行消费
LockSupport.unpark(p.waiter);
return LinkedTransferQueue.<E>cast(item);
}
}
//标注2
Node n = p.next;
p = (p != n) ? n : (h = head);
}
//标注3
if (how != NOW) {
if (s == null)
s = new Node(e, haveData);
Node pred = tryAppend(s, haveData);
if (pred == null)
continue retry;
if (how != ASYNC)
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e;
}
标注1:当找到一个节点是一个消费者节点,将值设置成功后,需要将head变为这个节点的后面一个节点,因为只有在链表为空的情况下才会添加消费者节点,当完成一个设置完一个消费者节点后就断开后面的连接并设置head,有助于GC回收这个节点
标注2:继续寻找下一个节点,如果这个过程中被其他线程给填充了数据了,则又从head开始寻找
标注3:如果没有找到一个消费节点,说明还没有消费者,这个时候根据how的值来控制逻辑:
- NOW 不插入节点直接返回,必须得有消费者才能成功插入
- SYNC 插入一个节点,在被取消或者匹配之前一直不返回
- ASYNC 插入后立即返回
- TIMED SYNC的超时模式
private Node tryAppend(Node s, boolean haveData) {
for (Node t = tail, p = t;;) {
Node n, u;
//如果head为空直接设置head
if (p == null && (p = head) == null) {
if (casHead(null, s))
return s;
}
else if (p.cannotPrecede(haveData))
return null;
//如果这个节点不是最后一个(说明中途被加入了)
else if ((n = p.next) != null)
p = p != t && t != (u = tail) ? (t = u) :
(p != n) ? n : null;
//CAS next失败 继续下一个节点
else if (!p.casNext(null, s))
p = p.next;
else {
//不断设置tail 失败了则重新设置tail继续
if (p != t) {
while ((tail != t || !casTail(t, s)) &&
(t = tail) != null &&
(s = t.next) != null && // advance and retry
(s = s.next) != null && s != t);
}
return p;
}
}
}
tryAppend两种模式对应了消费模式和添加模式,采用自旋方式将自身设置为tail
private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) {
// 超时时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
// 自旋次数
int spins = -1;
// 随机数
ThreadLocalRandom randomYields = null;
for (;;) {
Object item = s.item;
// 消费线程匹配了该节点
if (item != e) {
s.forgetContents();
return LinkedTransferQueue.<E>cast(item);
}
// 线程中断或者超时,则将s的节点item设置为s
if ((w.isInterrupted() || (timed && nanos <= 0)) &&
s.casItem(e, s)) {
// 断开节点
unsplice(pred, s);
return e;
}
if (spins < 0) {
// 计算自旋次数
if ((spins = spinsFor(pred, s.isData)) > 0)
randomYields = ThreadLocalRandom.current();
}
else if (spins > 0) { // spin
--spins;
// 生成随机数来让出CPU时间
if (randomYields.nextInt(CHAINED_SPINS) == 0)
Thread.yield();
}
// 将s的waiter设置为当前线程
else if (s.waiter == null) {
s.waiter = w;
}
// 带超时时间的阻塞
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos > 0L)
LockSupport.parkNanos(this, nanos);
}
// 非超时阻塞
else {
LockSupport.park(this);
}
}
}
等待这个节点被匹配,对应了take方法的逻辑
++节点的的take和put都是xfer方法,根据now传递的参数不同有不同的行为。++
结尾
本文介绍了各种同步的Queue实现方法,根据需求来使用。