JDK-In-Action-BlockingQueue
阻塞队列 BlockingQueue
阻塞队列操作方法概览
-|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|
ArrayBlockingQueue
特性
- 同步策略: 重入锁(可选公平/非公平)和非空(notEmpty), 非满(notFull)条件同步
并发操作前需要先获取独占锁;添加元素后,非空条件唤醒;移除元素后,非满条件唤醒; - 内部数据结构: 固定大小的环型数组(数组的第一位和最后一位逻辑相邻)
数组维护takeIndex用于移除/获取队首元素,putIndex用于添加队尾元素.当索引移动到数组最后时,从0再开始
移除队首元素只需要移动索引,但是删除中间的元素则需要进行拷贝移动元素 - 容量: 固定有界.初始构造时传入容量大小,同时创建数组分配内存
- 顺序: 先进先出FIFO有序
- 操作: 队头队尾元素操作在常数时间完成;支持阻塞或者超时的元素新增和移除
- 迭代: 弱一致性,可以与其他操作并发执行,可以删除元素.不会抛出
ConcurrentModificationException
结构示意图
三种构造函数
//仅设置容量, 默认非公平模式
BlockingQueue<Integer> blockingQueue1 = new ArrayBlockingQueue<>(4);
//设置容量和锁竞争模式
BlockingQueue<Integer> blockingQueue2 = new ArrayBlockingQueue<>(4, true);
//从一个集合中初始化队列元素
BlockingQueue<Integer> blockingQueue3 = new ArrayBlockingQueue<>(4, true, Arrays.asList(1, 2, 3, 4));
Draining 队列
当没有并发操作后,导出队列剩余的全部元素到集合
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(4, false, Arrays.asList(1, 2, 3, 4));
ArrayList<Integer> list = new ArrayList<>();
blockingQueue.drainTo(list);
assertEquals(list, "[1, 2, 3, 4]");
超时式元素操作
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
ExecutorService executor = newCommonExecutor();
final Integer loop = 3;
executor.submit(() -> {
try {
int n = loop;
while (n-- > 0) {
//获取并移除队首元素,空队列则等待
Integer value = blockingQueue.poll(1, TimeUnit.SECONDS);
log("poll:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try {
Random random = new Random();
int n = loop;
while (n-- > 0) {
int value = random.nextInt(100);
//队尾添加元素,满队列则等待
blockingQueue.offer(value, 1, TimeUnit.SECONDS);
log("offer:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
shutdownAndAwaitStop(executor);
17:43:52.297 Thread[pool-1-thread-1,5,main] poll:87
17:43:52.297 Thread[pool-1-thread-2,5,main] offer:87
17:43:52.330 Thread[pool-1-thread-1,5,main] poll:80
17:43:52.330 Thread[pool-1-thread-2,5,main] offer:80
17:43:52.330 Thread[pool-1-thread-2,5,main] offer:57
17:43:52.330 Thread[pool-1-thread-1,5,main] poll:57
阻塞式元素操作
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(4);
ExecutorService executor = newCommonExecutor();
final Integer loop = 5;
executor.submit(() -> {
try {
int n = loop;
while (n-- > 0) {
//获取并移除队首元素,空队列则等待
Integer value = blockingQueue.take();
log("take:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try {
Random random = new Random();
int n = loop;
while (n-- > 0) {
int value = random.nextInt(100);
//队尾添加元素,满队列则等待
blockingQueue.put(value);
log("put:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
shutdownAndAwaitStop(executor);
17:43:52.340 Thread[pool-2-thread-2,5,main] put:74
17:43:52.341 Thread[pool-2-thread-2,5,main] put:58
17:43:52.340 Thread[pool-2-thread-1,5,main] take:74
17:43:52.341 Thread[pool-2-thread-2,5,main] put:44
17:43:52.341 Thread[pool-2-thread-1,5,main] take:58
17:43:52.341 Thread[pool-2-thread-2,5,main] put:84
17:43:52.341 Thread[pool-2-thread-2,5,main] put:24
17:43:52.341 Thread[pool-2-thread-1,5,main] take:44
17:43:52.342 Thread[pool-2-thread-1,5,main] take:84
17:43:52.342 Thread[pool-2-thread-1,5,main] take:24
LinkedBlockingQueue
特性
- 数据结构: 固定容量的单向链表
- 同步策略: 使用两个重入锁和对应的套件同步
使用takeLock重入锁和非空(notEmpty)条件用于take, poll等操作的同步.
使用putLock重入锁和非满(notFull)条件用于put, offer等操作的同步.- 新增元素时, 获取putLock锁, 自旋等待非满条件(若定义超时, 则等待超时时间), 然后添加元素, 如果添加的是当前队列第一个元素则唤醒非空条件;
- 移除元素时, 获取takeLock锁, 自旋等待非空条件(若定义超时, 则等待超时时间), 然后移除元素, 如果还有剩余元素, 唤醒非空条件;如果是自满容量后第一个移除的元素, 唤醒非满条件;
- 顺序: FIFO有序
- 迭代: 弱一致性,可以与其他操作并发执行,可以删除元素.不会抛出
ConcurrentModificationException
结构示意图
API结果和ArrayBlockingQueue
一致,可以直接套用前面ArrayBlockingQueue
的示例
二种构造函数
//设置容量
BlockingQueue<Integer> blockingQueue1 = new LinkedBlockingQueue<>(4);
//从一个集合中初始化队列元素
BlockingQueue<Integer> blockingQueue3 = new LinkedBlockingQueue<>(Arrays.asList(1, 2, 3, 4));
PriorityBlockingQueue
特性
- 数据结构: 无界线性表结构的平衡二叉堆(最小堆/最大堆)
- 同步策略: 使用单个重入锁和非空(notEmpty)条件同步
- 顺序: 出队有序(最大/最小), 元素排序算法和
PriorityQueue
一致, 参见JDK-In-Action-PriorityQueue - 迭代: 弱一致性,可以并发操作,永远不会抛出
ConcurrentModificationException
,迭代无序
结构示意图
构造方法
//构造一个容量为4的优先级队列,按自然顺序排序
PriorityBlockingQueue<Integer> priorityBlockingQueue1 = new PriorityBlockingQueue<>(4);
//提供一个比较器,按整数逆序排序
PriorityBlockingQueue<Integer> priorityBlockingQueue2 = new PriorityBlockingQueue<>(4, (i1, i2) -> i2 - i1);
//从集合构造优先级队列
final List<Integer> list = Arrays.asList(1, 5, 2, 6, 8);
PriorityBlockingQueue<Integer> priorityBlockingQueue3 = new PriorityBlockingQueue<>(list);
assertEquals(priorityBlockingQueue3.toString(), "[1, 5, 2, 6, 8]");
有序地读队首元素
BlockingQueue<Integer> blockingQueue = new PriorityBlockingQueue<>(4);
try {
blockingQueue.put(3);
blockingQueue.put(5);
blockingQueue.put(2);
blockingQueue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer value = blockingQueue.poll();
while (value != null) {
log("poll:" + value);
value = blockingQueue.poll();
}
19:51:57.175 Thread[main,5,main] poll:1
19:51:57.175 Thread[main,5,main] poll:2
19:51:57.175 Thread[main,5,main] poll:3
19:51:57.175 Thread[main,5,main] poll:5
基于优先级队列的生产者消费者模型
生产者无序推送元素,消费者优先获取小元素
BlockingQueue<Integer> blockingQueue = new PriorityBlockingQueue<>(4);
ExecutorService executor = newCommonExecutor();
final Integer loop = 5;
executor.submit(() -> {
try {
int n = loop;
while (n-- > 0) {
//获取并移除队首元素,空队列则等待
Integer value = blockingQueue.take();
log("take:" + value);
sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try {
Random random = new Random();
int n = loop;
while (n-- > 0) {
int value = random.nextInt(100);
//队尾添加元素,满队列则等待
blockingQueue.put(value);
log("put:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
shutdownAndAwaitStop(executor);
19:51:56.651 Thread[pool-1-thread-1,5,main] take:52
19:51:56.651 Thread[pool-1-thread-2,5,main] put:52
19:51:56.671 Thread[pool-1-thread-2,5,main] put:62
19:51:56.671 Thread[pool-1-thread-2,5,main] put:29
19:51:56.671 Thread[pool-1-thread-2,5,main] put:96
19:51:56.671 Thread[pool-1-thread-2,5,main] put:77
19:51:56.771 Thread[pool-1-thread-1,5,main] take:29
19:51:56.872 Thread[pool-1-thread-1,5,main] take:62
19:51:56.972 Thread[pool-1-thread-1,5,main] take:77
19:51:57.073 Thread[pool-1-thread-1,5,main] take:96
DelayQueue
特性
- 内部数据结构: 优先级队列(平衡二叉堆)
- 同步策略: 使用
ReentrantLock
同步 - 容量: 无界,所以put,offer不会阻塞
- 顺序: 队列根据
getDelay()
方法返回的剩余延迟时间对其元素进行排序. 队列的头部包含剩余延迟时间最少的元素. 队列的尾部包含剩余延迟时间最长的元素. - 操作:
- 迭代器: 弱一致性,无序
实现细节
- 队列元素必须实现
Delayed
接口提供一个获取延时时间的方法getDelay
- 领导者与跟随者:
leader
指定为等待队列头部元素的线程. 领导者-跟随者模式的这种变体可以最小化不必要的定时等待.当一个线程成为leader时, 它只等待下一次延迟的到来, 而其他线程则无限期地等待. - offer: 插入元素; 首先获取独占锁,插入元素,如果插入元素是最小的,释放leader,唤醒其他等待线程.否则直接返回true(插入成功)
- poll: 检索和删除队列中过期的元素,如果不存在则返回null.获取独占锁,peek优先队列的堆顶元素,判断是否过期,没有则返回null,否则移除并返回堆顶元素
- take: 检索和删除队列中过期的元素;如果不存在则等待队列可用;如果未过期则计算剩余等待的时间然后等待或者超时等待,等待前判断能否成为leader,成为leader则超时等待,否则始终等待直到被唤醒;
延时队列调度
DelayQueue<DT> delayQueue = new DelayQueue<>();
Arrays.asList(12, 15, 14, 16).stream().forEach(t -> {
delayQueue.put(new DT(t));
});
try {
DT top = delayQueue.poll(3000, TimeUnit.MILLISECONDS);
while (top != null) {
log("run:" + top);
top = delayQueue.poll(3000, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
21:06:32.962 Thread[main,5,main] run:[12]
21:06:33.114 Thread[main,5,main] run:[14]
21:06:33.214 Thread[main,5,main] run:[15]
21:06:33.315 Thread[main,5,main] run:[16]
SynchronousQueue
特性
- 无容量的阻塞同步队列
- 生产者投递元素必须有一个消费者接受才返回,否则阻塞;元素直接被转移到消费者,不经过队列存储;
- 顺序: 可选公平模式(用队列排队线程)或者非公平模式(用栈排队线程)
基于同步队列的生产者和消费者
可以看到输出文本中,每一个take后紧跟着一个take
final SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();
final ExecutorService executor = newCommonExecutor();
final Integer loop = 2;
executor.submit(() -> {
try {
Random random = new Random();
int n = loop;
while (n-- > 0) {
int value = random.nextInt(100);
synchronousQueue.put(value);
log("put:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.submit(() -> {
try {
int n = loop;
while (n-- > 0) {
Integer value = synchronousQueue.take();
log("take:" + value);
sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
shutdownAndAwaitStop(executor);
21:15:33.081 Thread[pool-1-thread-1,5,main] put:56
21:15:33.081 Thread[pool-1-thread-2,5,main] take:56
21:15:33.202 Thread[pool-1-thread-1,5,main] put:53
21:15:33.202 Thread[pool-1-thread-2,5,main] take:53
TransferQueue
一种特殊的阻塞队列, 生产者可能等待消费者处理.在消息传递的应用程序中非常有用, 使用transfer
方法投递元素, 但是仅当此前所有元素全部消费后, 该方法才返回.
一个容量为0的TransferQueue
的transfer
方法和SynchronousQueue
的put
等同.
特殊API
boolean tryTransfer()
: 如果可能, 立即将元素传输给等待的使用者. 更精确地说, 如果存在一个消费者已经在等待接收指定的元素(在take或timed poll中), 则立即传输该元素, 否则将返回false, 而不会对该元素进行排队.void transfer() throws InterruptedException
: 将元素传输给使用者, 如有必要则等待. 更精确地说, 如果存在一个消费者已经在等待接收指定的元素(在take或timed poll中), 则立即传输指定的元素, 否则将等待, 直到该元素被消费者接收.boolean hasWaitingConsumer()
: 如果至少有一个消费者在等待通过take或timed pool接收元素, 则返回true. 返回值代表事件的瞬间状态.int getWaitingConsumerCount()
: 返回通过take或定时轮询等待接收元素的消费者数量的估计值. 返回值是事件瞬间状态的近似值, 如果消费者已经完成或放弃等待, 则返回值可能不准确. 该值对于监视和启发可能有用, 但对于同步控制则没用. 此方法的实现可能比hasWaitingConsumer的实现要慢得多.
LinkedTransferQueue
特性
- 数据结构: 无界的无锁双端队列
- 顺序: FIFO
- 特殊的,size()方法不是一个常量时间操作,因为队列的异步性,确定元素个数需要遍历所有元素,如果期间发生修改,报告结果将不准确
- 不保证批量操作addAll、removeAll、retainAll、containsAll、equals和toArray以原子方式执行.
例如,与addAll操作并发操作的迭代器可能只查看添加的部分元素 - 元素不允许为null
- 迭代: 弱一致性
分批确保交付生产消费
每个批次发送三个数据,同时确保每个批次被消费后才发送下一批次
final TransferQueue<Integer> queue = new LinkedTransferQueue<>();
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
queue.put(1);
queue.put(2);
queue.transfer(3);
System.out.println("transfer----done");
}
} catch (Exception e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
for (int i = 0; i < 9; i++) {
System.out.println("take:" + queue.take());
}
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
take:1
take:2
take:3
transfer----done
take:1
take:2
take:3
transfer----done
take:1
take:2
take:3
transfer----done
阻塞双端队列 BlockingDeque
双端队列操作汇总
- First Element (Head)
- | Throws exception | Special value | Blocks | Times out |
---|---|---|---|---|
Insert | addFirst(e) | offerFirst(e) | putFirst(e) | offerFirst(e, time, unit) |
Remove | removeFirst() | pollFirst() | takeFirst() | pollFirst(time, unit) |
Examine | getFirst() | peekFirst() | not applicable | not applicable |
- Last Element (Tail)
- | Throws exception | Special value | Blocks | Times out |
---|---|---|---|---|
Insert | addLast(e) | offerLast(e) | putLast(e) | offerLast(e, time, unit) |
Remove | removeLast() | pollLast() | takeLast() | pollLast(time, unit) |
Examine | getLast() | peekLast() | not applicable | not applicable |
也阻塞队列API的方法相当与双端队列的First Element (Head)系列方法(见上文阻塞队列)
LinkedBlockingDeque
特性
- 内部数据结构: 有界双向链表
- 同步策略: 使用重入锁(可选公平/非公平)和非空(notEmpty), 非满(notFull)条件同步.
- 可以从队首或者队尾操作队列,队列满或者队列空时,响应的操作阻塞等待必要条件
结构图
阻塞式操作双端队列
LinkedBlockingDeque<Integer> deque = new LinkedBlockingDeque(8);
int loop = 4;
try {
int n = loop;
while (n-- > 0) {
deque.putFirst(n);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
int n = loop;
while (n-- > 0) {
deque.putLast(n * 100 + 1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
log("Queue:" + deque.toString());
try {
int n = loop * 2;
while (n-- > 0) {
if (n % 2 == 0) {
final Integer last = deque.pollFirst(3, TimeUnit.SECONDS);
if (last == null) break;
log("pollFirst:" + last);
} else {
final Integer first = deque.pollLast(3, TimeUnit.SECONDS);
if (first == null) break;
log("pollLast:" + first);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
22:06:06.961 Thread[main,5,main] Queue:[0, 1, 2, 3, 301, 201, 101, 1]
22:06:06.981 Thread[main,5,main] pollLast:1
22:06:06.981 Thread[main,5,main] pollFirst:0
22:06:06.981 Thread[main,5,main] pollLast:101
22:06:06.981 Thread[main,5,main] pollFirst:1
22:06:06.981 Thread[main,5,main] pollLast:201
22:06:06.981 Thread[main,5,main] pollFirst:2
22:06:06.982 Thread[main,5,main] pollLast:301
22:06:06.982 Thread[main,5,main] pollFirst:3