BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:
一个线程往里边放,另外一个线程从里边取。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限
的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,
直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试
去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到
立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
注意:无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个NullPointerException。
BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,Java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:
ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现java.util.concurrent.Delayed 接口。
LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
PriorityBlockingQueue : PriorityBlockingQueue 是 一 个 无 界 的 并 发 队 列 。 它 使 用 了 和 类java.util.PriorityQueue 一 样 的 排 序 规 则 。 你 无 法 向 这 个 队 列 中 插 入 null 值 。 所 有 插 入 到PriorityBlockingQueue 的元素必须实现java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
下面是 ArrayBlockingQueue 的类图
如上图 ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标,takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以进行入队出队操作,另外 notEmpty,notFull 条件变量用来进行出入队的同步。
另外构造函数必须传入队列大小参数,所以为有界队列,默认是 Lock 为非公平锁。
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();
}
就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
offer方法:
在队尾插入元素,如果队列满则返回 false,否者入队返回 true。
public boolean offer(E e) {
//e 为 null,则抛出 NullPointerException 异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列满则返回 false
if (count == items.length){
return false;
} else {
//否者插入元素
insert(e);
return true;
}
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
//元素入队
items[putIndex] = x;
//计算下一个元素应该存放的下标
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是在 CPU 缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。另外 insert 后调用notEmpty.signal();是为了激活调用notEmpty.await()阻塞后放入 notEmpty 条件队列中的线程。
Put 方法:
在队列尾部添加元素,如果队列满则等待队列有空位置后,再次插入并返回,阻塞等待。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁
lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入 notFull 管理的条件队列
while (count == items.length)
notFull.await();
//插入元素
insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了 notFull.signal 方法激活该线程。代码逻辑很简单,但是这里需要思考一个问题为啥调用 lockInterruptibly 方法而不是 Lock 方法。我的理解是因为调用了条件变量的 await()方法,而 await()方法会在中断标志设置后抛出 InterruptedException 异常后退出,所以还不如在加锁时候先看中断标志是不是被设置了,如果设置了直接抛出 InterruptedException 异常,就不用再去获取锁了。然后看了其他并发类里面凡是调用了 await 的方法获取锁时候都是使用的 lockInterruptibly 方法而不是 Lock 也验证了这个想法。
Poll方法:
从队头获取并移除元素,队列为空,则返回 null。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//当前队列为空则返回 null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.cast(items[takeIndex]);
//数组中值值为 null;
items[takeIndex] = null;
//队头指针计算,队列元素个数减一
takeIndex = inc(takeIndex);
--count;
//发送信号激活 notFull 条件队列里面的线程
notFull.signal();
return x;
}
}
take方法:
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空,当前线程会被挂起放到 notEmpty 的条件队列里面,直到入队操作执行调用notEmpty.signal 后当前线程才会被激活,await 才会返回。
peek方法:
返回队列头元素但不移除该元素,队列为空,返回 null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列为空返回 null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.cast(items[i]);
}
size方法:
获取队列元素个数,非常精确。因为计算 size 时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue 小结
ArrayBlockingQueue 通过使用全局独占锁实现,同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加 synchronized 的意味。其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take则使用了条件变量实现,如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了全局锁。
LinkedBlockingQueue 中也有两个 Node 分别用来存放首尾节点,并且里面有个初始值为 0 的原子变量 count用来记录队列元素个数,另外里面有两个 ReentrantLock 的独占锁,分别用来控制元素入队和出队加锁,其中 takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock 控制同时只能有一个线程可以获取锁
去添加元素,其他线程必须等待。另外 notEmpty 和 notFull 用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型,如下类图:
/** 通过 take 取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/*通过 put 放置进行加锁、放置*/
private final ReentrantLock putLock = new ReentrantLock();
/** 等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/* 记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
//队列初始容量,Integer 最大值
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//初始化首尾节点
last = head = new Node(null);
}
如图默认队列容量为 0x7fffffff;用户也可以自己指定容量。
带时间的 Offer 操作-生产者:
在 ArrayBlockingQueue 中已经简单介绍了 Offer()方法,LinkedBlocking 的 Offer 方法类似,在此就不过多去介绍,这次我们介绍带时间的 Offer 方法。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
//空元素抛空指针异常
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取可被中断锁,只有一个线程可获取
putLock.lockInterruptibly();
try {
//如果队列满则进入循环
while (count.get() == capacity) {//nanos<=0 直接返回
if (nanos <= 0)
return false;
//否者调用 await 进行等待,超时则返回<=0(1)
nanos = notFull.awaitNanos(nanos);
}
//await 在超时时间内返回则添加元素(2)
enqueue(new Node(e));
c = count.getAndIncrement();
//队列不满则激活其他等待入队线程(3)
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//c==0 说明队列里面有一个元素,这时候唤醒出队线程(4)
if (c == 0)
signalNotEmpty();
return true;
}
private void enqueue(Node node) {
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
带时间的 poll 操作-消费者
获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素,如果有则返回,否者超时后返回 null,具体代码请看相关的源码。
put 操作-生产者
与带超时时间的 offer 类似不同在于 put 时候如果当前队列满了它会一直等待其他线程调用 notFull.signal 才会被唤醒。
take 操作-消费者
与带超时时间的 poll 类似不同在于 take 时候如果当前队列空了它会一直等待其他线程调用 notEmpty.signal()才会被唤醒。
size 操作-消费者
当前队列元素个数,如代码直接使用原子变量 count 获取。
public int size() {
return count.get();
}
peek 操作
获取但是不移除当前队列的头元素,没有则返回 null。
public E peek() {
//队列空,则返回 null
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
remove 操作
删除队列里面的一个元素,有则删除返回 true,没有则返回 false,在删除操作时候由于要遍历队列所以加了双重锁,也就是在删除过程中不允许入队也不允许出队操作。
仔细思考下阻塞队列是如何实现并发安全的维护队列链表的,先分析下简单的情况就是当队列里面有多个元素时候,由于同时只有一个线程(通过独占锁 putLock 实现)入队元素并且是操作 last 节点,而同时只有一个出队线程(通过独占锁 takeLock 实现)操作 head 节点,所以不存在并发安全问题。
考虑当队列为空的时候队列状态为:
这 时 候 假 如 一 个 线 程 调 用 了 take 方 法 , 由 于 队 列 为 空 , count.get()==0, 所 以 当 前 线 程 会 调 用notEmpty.await() 把 自 己 挂 起 , 并 且 放 入 notEmpty 的 条 件 队 列 , 并 且 释 放 当 前 条 件 变 量 关 联 的 通 过takeLock.lockInterruptibly()获取的独占锁。由于释放了锁,所以这时候其他线程调用 take 时候就会通过takeLock.lockInterruptibly()获取独占锁,然后同样阻塞到 notEmpty.await(),同样会被放入 notEmpty 的条件队列,也就说在队列为空的情况下可能会有多个线程因为调用 take 被放入了 notEmpty 的条件队列。
这时候如果有一个线程调用了 put 方法,那么就会调用 enqueue 操作,该操作会在 last 节点后面添加新元素并且设置 last 为新节点。然后 count.getAndIncrement()先获取当前队列元个数为 0 保存到 c,然后自增 count 为 1,由于 c==0 ,所以调用 signalNotEmpty 激活 notEmpty 的条件队列里面的阻塞时间最长的线程,这时候 take 中调用
notEmpty.await()的线程会被激活, await 内部会重新去获取独占锁,获取成功则返回,否者被放入 AQS 的阻塞队列,如果获取成功,那么 count.get() >0, 因为可能多个线程 put 了,所以调用 dequeue 从队列获取元素(这时候一定可以获取到),然后调用 c = count.getAndDecrement() 把当前计数返回后并减去 1,如果 c>1 说明当前队列还有其他
元素,那么就调用 notEmpty.signal()去激活 notEmpty 的条件队列里面的其他阻塞线程。
考虑当队列满的时候:
当队列已满并且调用 put 方法时候,由于 notFull.await(),当前线程被阻塞放入 notFull 管理的条件队列里面,同理可能会有多个调用 put 方法的线程都放到了 notFull 的条件队列里面。这时候如果有一个线程调用了 take 方法,调用 dequeue()出队一个元素,c = count.getAndDecrement();count值减一;c==capacity;现在队列有一个空的位置,所以调用 signalNotFull()激活 notFull 条件队列里面等待最久的一个线程。
PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现,研究过数组方式存放最小堆节点的都知道,直接遍历队列元素是无序的。
PriorityBlockingQueue 类图结构:
如图 PriorityBlockingQueue 内部有个数组 queue 用来存放队列元素,size 用来存放队列元素个数,allocationSpinLockOffset 是用来在扩容队列时候做 cas 的,目的是保证只有一个线程可以进行扩容。
由于这是一个优先级队列所以有个比较器 comparator 用来比较元素大小。lock 独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty 条件变量用来实现 take 方法阻塞模式。这里没有 notFull 条件变量是因为这里的 put 操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列,最后 PriorityQueue q 用来搞序列化的。
如下构造函数,默认队列容量为 11,默认比较器为 null;
private static final int DEFAULT_INITIAL_CAPACITY = 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];
}
Offer 操作
在队列插入一个元素,由于是无界队列,所以一直为成功返回 true,再添加元素的过程中,如果元素的个数大于队列的初始容量大小,队列就要进行扩容,具体请看PriorityBlockingQueue 的offer方法的源码,这里不做赘述。
源码中tryGrow方法的目的是扩容,这里要思考下为啥在扩容前要先释放锁,然后使用 cas 控制一个线程进行扩容操作。我的理解是为了性能,因为扩容时候是需要花时间的,如果这些操作时候还占用锁,那么其他线程在这个时候是不能进行出队操作的,也不能进行入队操作,这大大降低了并发性。
所以在扩容前释放锁,这允许其他出队线程可以进行出队操作,但是由于释放了锁,所以也允许在扩容时候进行入队操作,这就会导致多个线程进行扩容会出现问题,所以这里使用了一个 spinlock 用 cas 控制只有一个线程可以进行扩容,失败的线程调用 Thread.yield()让出 cpu,目的意在让扩容线程扩容后优先调用 lock.lock 重新获取锁,但是这得不到一定的保证,有可能调用 Thread.yield()的线程先获取了锁,yield()方法只能释放cpu资源,不能释放对象监视器(锁资源)。
那 copy 元素数据到新数组为啥放到获取锁后面呢?原因应该是因为可见性问题,因为 queue 并没有被 volatile 修饰。另外有可能在扩容时候进行了出队操作,如果直接拷贝可能看到的数组元素不是最新的。而通过调用 Lock 后,获取的数组则是最新的,并且在释放锁前数组内容不会变化。
Poll 操作
在队列头部获取并移除一个元素,如果队列为空,则返回 null。
Put 操作
内部调用的 offer,由于是无界队列,所以不需要阻塞。
Take 操作
获取队列头元素,如果队列为空则阻塞。
Size 操作
获取队列元个数,由于加了独占锁所以返回结果是精确的。
PriorityBlockingQueue 类似于 ArrayBlockingQueue 内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队,另外前者只使用了一个 notEmpty 条件变量而没有 notFull 这是因为前者是无界队列,当 put 时候永远不会处于 await 所以也不需要被唤醒。PriorityBlockingQueue 始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当当前元素个数>=最大容量时候会通过算法扩容。值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过 cas 保证同时只有一个线程可以扩容成功。
PriorityBlockingQueue 类是 JDK 提供的优先级队列 本身是线程安全的 内部使用显示锁 保证线程安全。PriorityBlockingQueue 存储的对象必须是实现 Comparable 接口的 因为 PriorityBlockingQueue 队列会根据内部存储的每一个元素的 compareTo 方法比较每个元素的大小。这样在 take 出来的时候会根据优先级 将优先级最小的最先取出 。
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
如下面的示意图所示:
SynchronousQueue 的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put 的时候),如果当前没有人想要消费产品(即当前没有线程执行 take),此生产线程必须阻塞,等待一个消费线程调用 take 操作,take 操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先 take 后 put,原理是一样的)。
如下代码:
package com.thread.queue;
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue queue = new SynchronousQueue<>();
Thread putThread = new Thread(() -> {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
});
Thread takeThread = new Thread(() -> {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
运行结果如下:
put thread start
take thread start
put thread end
take from putThread: 1
take thread end
从结果可以看出,put 线程执行 queue.put(1) 后就被阻塞了,只有 take 线程进行了消费,put 线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。
ArrayBlockingQueue 、 LinkedBlockingDeque 之 类 的 阻 塞 队 列 依 赖 AQS 实 现 并 发 操 作 ,SynchronousQueue 直接使用 CAS 实现线程的安全访问。
队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。
公平模式下的模型:
公平模式下,底层实现使用的是 TransferQueue 这个内部队列,它有一个 head 和 tail 指针,用于指向当前正在等待匹配的线程节点。
初始化时,TransferQueue 的状态如下:
接着我们进行一些操作:
1、线程 put1 执行 put(1)操作,由于当前没有配对的消费线程,所以 put1 线程入队列,自旋一小会后睡眠等待,这时队列状态如下:
2、接着,线程 put2 执行了 put(2)操作,跟前面一样,put2 线程入队列,自旋一小会后睡眠等待,这时队列状态如下:
3、这时候,来了一个线程 take1,执行了 take 操作,由于 tail 指向 put2 线程,put2 线程跟 take1 线程配对了(一 put 一 take),这时 take1 线程不需要入队,但是请注意了,这时候,要唤醒的线程并不是 put2,而是 put1。
为何? 大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显是put1 应该优先被唤醒。至于读者可能会有一个疑问,明明是 take1 线程跟 put2 线程匹配上了,结果是 put1 线程被唤醒消费,怎么确保 take1 线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配,队头出队。
执行后 put1 线程被唤醒,take1 线程的 take()方法返回了 1(put1 线程的数据),这样就实现了线程间的一对一通信,这时候内部状态如下:
4、最后,再来一个线程 take2,执行 take 操作,这时候只有 put2 线程在等候,而且两个线程匹配上了,线程 put2 被唤醒, take2 线程 take 操作返回了 2(线程 put2 的数据),这时候队列又回到了起点,如下所示:
以上便是公平模式下,SynchronousQueue 的实现模型。总结下来就是:队尾匹配,队头出队,执行put操作的线程先进先出,当存在take线程执行take操作的时候,take线程取的是优先进入队列的put线程的数据,体现公平原则。
非公平模式下的模型:
非公平模式底层的实现使用的是TransferStack,一个栈,实现中用 head 指针指向栈顶,接着我们看看它的实现模型:
1、线程 put1 执行 put(1)操作,由于当前没有配对的消费线程,所以 put1 线程入栈,自旋一小会后睡眠等待,这时栈状态如下:
2、接着,线程 put2 再次执行了 put(2)操作,跟前面一样,put2 线程入栈,自旋一小会后睡眠等待,这时栈状态如下:
3、这时候,来了一个线程 take1,执行了 take 操作,这时候发现栈顶为 put2 线程,匹配成功,但是实现会先把 take1 线程入栈,然后 take1 线程循环执行匹配 put2 线程逻辑,一旦发现没有并发冲突,就会把栈顶指针直接指向 put1 线程。
4、最后,再来一个线程 take2,执行 take 操作,这跟步骤 3 的逻辑基本是一致的,take2 线程入栈,然后在循环中匹配 put1 线程,最终全部匹配完毕,栈变为空,恢复初始状态,如下图所示:
步骤二:
可以从上面流程看出,虽然 put1 线程先入栈了,但是却是后匹配,这就是非公平的由来。
SynchronousQueue 由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用 AQS,而是直接使用 CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。
DelayQueue 是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。
DelayQueue 阻塞队列在我们系统开发中也常常会用到,例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;任务调度系统,能够准确的把握任务的执行时间。我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,我们就需要遍历所有的对象,一个一个的检查看数据是否过期等,首先这样在
执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。一个需要 12:00 点执行的任务可能12:01 才执行,这样对数据要求很高的系统有更大的弊端。由此我们可以使用 DelayQueue。
DelayQueue 是一个 BlockingQueue,其特化的参数是 Delayed。
Delayed 扩展了 Comparable 接口,比较的基准为延时的时间值,Delayed 接口的实现类 getDelay 的返回值应为固定值(final)。DelayQueue 内部是使用 PriorityQueue 实现的。
DelayQueue = BlockingQueue +PriorityQueue + Delayed
DelayQueue 的关键元素 BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue 是一个使用优先队列(PriorityQueue)实现的 BlockingQueue,优先队列的比较基准值是时间。
他们的基本定义如下:
public interface Comparable {
public int compareTo(T o);
}
public interface Delayed extends Comparable {
long getDelay(TimeUnit unit);
}
public class DelayQueue implements BlockingQueue {
private final PriorityQueue q = new PriorityQueue();
}
DelayQueue 内部的实现使用了一个优先队列。当调用 DelayQueue 的 offer 方法时,把 Delayed 对象加入到优先队列 q 中。如下:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}
DelayQueue 的 take 方法,把优先队列 q 的 first 拿出来(peek),如果没有达到延时阀值,则进行 await处理。如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
} } }
} finally {
lock.unlock();
}
}