本文主要对Java常用阻塞队列进行介绍和提供相关使用案例
阻塞队列提供了一种线程安全、高效的数据传递和同步机制 , 主要用于缓冲数据、限流、削峰填谷,生产者-消费者模型,线程间的协作等等。
队列 | 有界性 | 锁 | 锁方式 | 数据结构 |
---|---|---|---|---|
ArrayBlockingQueue | 有 | 有锁 | ReentrantLock | 数组 |
LinkedBlockingQueue | 有界 | 有锁 | 两个锁ReentrantLock + 条件变量Condition | 双向链表 |
LinkedTransferQueue | 无界 | 无锁 | CAS+原子变量 | 链表 |
PriorityBlockingQueue | 无界 | 有锁 | 独占锁(ReentrantLock) | 优先级队列(DelayWorkQueue) |
DelayQueue | 无界 | 有锁 | ReentrantLock | 堆(PriorityQueue) |
SynchronousQueue | 无容量 | 无锁 | CAS+自旋(无锁),自旋了一定次数后调用 LockSupport.park()进行阻塞 | 链表 |
注意:
添加元素
add
: 如果队列已满,抛出 IllegalStateException
异常offer
:如果队列已满,falseput
: 如果队列已满,阻塞
等待直到队列有空闲位置删除元素
take
: 如果队列为空, 阻塞
等待直到队列有元素poll
: 如果队列为空,返回 nullremove
: 如果队列为空,抛出NoSuchElementException
异常drainTo(Collection):
批量从队列中取出全部元素到集合中drainTo(Collection, int)
: 批量从队列中取出n个元素到集合中remove(Obejct)
: 删除指定元素,删除成功返回true, 如果有多个相同元素只会删除一个removeIf(Predicate)
: 根据断言表达式删除所有符合条件的元素,删除失败返回falseremoveAll(Collection)
: 删除队列中所有在集合中存在的的元素,删除失败返回false (差集)retainAll(Collection)
: 保留队列中所有在集合中存在的的元素。 (交集)查看元素
peek:
查看队头元素, 如果队列为空, 返回nullelement
: 查看队头元素, 如果队列为空,抛出 NoSuchElementException
异常其他:
remainingCapacity:
返回队列可用容量大小isEmpty
: 队列是否为空包括ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue 、PriorityBlockingQueue这些队列在用法上无本质区别,只是底层数据结构和加锁方式不一样。
简单的生产者-消费者模型(1P-3C)使用
@Test
public void test4() throws InterruptedException {
// 编写1个生产者-3个消费者的模型
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 1个生产者
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
// 生产元素如果满了阻塞等待
queue.put("data_"+i);
System.out.println("生产者生产元素: " + i);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
// 3个消费者
for (int i = 0; i < 3; i++) {
final int index = i;
new Thread(() -> {
while (true){
try {
// 消费元素,如果队列为空阻塞等待
System.out.println("消费者"+index+"消费元素: " + queue.take());
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
Thread.sleep(300000);
}
同步阻塞队列
既然不会存储元素那它能干什么呢? 还是生产者-消费者模型, 与一般阻塞队列区别是每次生产者线程只能生产一份数据, 只有这份数据被消费者线程消费了,生产者才能继续生产。 同理,消费者线会阻塞等待生产者线程提供数据后才能进行处理。
ps
: 它容量不是 1 而是 0,因为它不需要去持有元素,它所做的就是直接传递而已它适合的逻辑执行链路是 生产-->消费--> 生产--> 消费-->生产--> 消费
.
假设有一个场景, 两个客户端端线程A和B, 线程A和B需要通过几次的信号同步才能建立连接成功,下面是三次信号同步的逻辑,
操作时间 | 操作 | 客户端A | 客户端B |
---|---|---|---|
1 | A向B建立连接 | queue.put(true) | queue.take() |
2 | B向A建立连接 | queue.take() | queue.put(true) |
3 | A向B建立连接 | queue.put(true) | queue.take(true) |
@Test
public void test22() throws InterruptedException {
SynchronousQueue<Boolean> queue = new SynchronousQueue<>(true);
AtomicInteger connectionCount = new AtomicInteger(0);
Client clientA = new Client(queue,connectionCount);
Client clientB = new Client(queue,connectionCount);
clientA.connectTo(clientB);
System.out.println("第" + connectionCount.get() +"次连接成功");
clientB.connectTo(clientA);
System.out.println("第" + connectionCount.get() +"次连接成功");
clientA.connectTo(clientB);
System.out.println("第" + connectionCount.get() +"次连接成功");
System.out.println("结束");
}
static class Client {
private SynchronousQueue<Boolean> queue;
private AtomicInteger connectionCount;
public Client(SynchronousQueue<Boolean> queue, AtomicInteger connectionCount) {
this.queue = queue;
this.connectionCount = connectionCount;
}
public void ack() throws InterruptedException {
this.queue.take();
}
public boolean connectTo(Client b) throws InterruptedException {
new Thread(() -> {
try {
Thread.sleep(2000);
b.ack();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
queue.put(true);
int count = connectionCount.addAndGet(1);
if (count >= 3){
return true;
}else {
return false;
}
}
}
假设有一个场景,有两夫妻, 第一天老公A负责卖鱼,赚到的钱给老婆B, 第二天老婆用这笔钱去投资,投资赚到的钱给老公,第三天老公用这笔钱去继续买鱼,如此日复一日, 夫妻两人属于隔天工作赚钱模式, 接下来我们用同步阻塞队列实现这个场景
@Test
public void test25() throws InterruptedException {
SynchronousQueue<AtomicInteger> queue = new SynchronousQueue<>();
Thread threadA = new Thread(() -> {
try {
new A(queue).startWork();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread threadB = new Thread(() -> {
try {
new B(queue).startWork();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
@Data
class A {
private SynchronousQueue<AtomicInteger> queue ;
// 老公的钱
AtomicInteger money = new AtomicInteger(1000);
public A(SynchronousQueue<AtomicInteger> queue) {
this.queue = queue;
}
public void startWork() throws InterruptedException {
while (true) {
// 卖鱼赚到的钱
Thread.sleep(2000);
int earnMoney = new Random().nextInt(100);
money.addAndGet(earnMoney);
System.out.println("老公赚到了" + earnMoney + "元, 当前余额: " + money.get());
// 把钱全部给老婆
queue.put(money);
System.out.println("老公开始休息");
// 等待老婆的钱
money = queue.take();
System.out.println("收到了老婆的" + money + "元, 继续卖鱼");
}
}
}
@Data
class B extends Thread {
private SynchronousQueue<AtomicInteger> queue ;
// 老婆的钱
AtomicInteger money = new AtomicInteger(0);
public B(SynchronousQueue<AtomicInteger> queue) {
this.queue = queue;
}
public void startWork() throws InterruptedException {
while (true) {
// 等待老公的钱
money = queue.take();
System.out.println("收到了老公的" + money + "元, 继续投资");
// 投资赚到的钱
Thread.sleep(2000);
int earnMoney = new Random().nextInt(100);
money.addAndGet(earnMoney);
System.out.println("老婆赚到了" + earnMoney + "元, 当前余额: " + money.get());
// 把钱全部给老公
queue.put(money);
System.out.println("老婆开始休息");
}
}
}
在指定容量为1的普通阻塞队列和SynchronousQueue有什么区别?
容量为1的普通阻塞队列在put第一个元素并不会阻塞等待,因为还没满,只有put第二个元素后因为队列满了才会阻塞等待。 而SynchronousQueue put就会直接阻塞等待。
SynchronousQueue适用于需要精确控制线程之间交换
、传递
元素的场景,而普通阻塞队列适用于需要缓冲
多个元素的场景
适合场景:
可以看作是SynchronousQueue和LinkedBlockingQueue的结合体。 即支持SynchronousQueue的直接传递性,减少用锁来同步,也支持普通无界阻塞队列的存储更多元素.
与普通阻塞队列区别就是多了一些以下的方法去添加元素
transfer方法
:
tryTransfer方法
hasWaitingConsumer
: 是否有消费者线程在等待
getWaitingConsumerCount
: 获取等待的消费者数量的
Delayed
接口的子类, 会根据Delayed接口的compareTo方法进行优先级排序,时间越小元素的将被优先取出。getDelay方法
来判断延迟时间是否到了然后取出, 这个方法返回值代表剩余的延迟时间,如果这个值小于等于0就被马上被取出消费。下面是一个使用案例:
public static void main(String[] args) {
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();
// 添加延迟任务
delayQueue.put(new DelayedTask("Task 1", 5, TimeUnit.SECONDS)); // 延迟5秒
delayQueue.put(new DelayedTask("Task 2", 10, TimeUnit.SECONDS)); // 延迟10秒
delayQueue.put(new DelayedTask("Task 3", 2, TimeUnit.SECONDS));
delayQueue.add(new DelayedTask("Task 4", 0, TimeUnit.SECONDS));
delayQueue.add(new DelayedTask("Task 5", 0, TimeUnit.SECONDS));
log.info("start");
// 处理延迟任务
while (!delayQueue.isEmpty()) {
try {
// 队头的元素延迟时间到了才会被取出否则一致阻塞等待
DelayedTask task = delayQueue.take();
log.info("处理任务: {}", task.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class DelayedTask implements Delayed {
private final String name;
private final long delay;
private final long expireTime;
DelayedTask(String name, long delay, TimeUnit unit) {
this.name = name;
this.delay = unit.toMillis(delay);
this.expireTime = System.currentTimeMillis() + this.delay;
}
String getName() {
return name;
}
// 返回剩余的延迟时间, 如果小于等于0则会被取出使用
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// 按照具体过期时间进行优先级排序,越小的在前面优先被取出
@Override
public int compareTo(Delayed other) {
return Long.compare(this.expireTime, ((DelayedTask) other).expireTime);
}
}
下面是执行结果,从打印时间可以看到,对应的任务都是到了指定的延迟时间才会被取出。
注意由于任务4和5指定的延迟时间为0所以会被马上取出处理
17:29:29.746 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - start
17:29:29.747 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - 处理任务: Task 4
17:29:29.748 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - 处理任务: Task 5
17:29:31.751 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - 处理任务: Task 3
17:29:34.749 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - 处理任务: Task 1
17:29:39.749 [main] INFO com.disruptor.blockQueue.BQTest3_DelayQueue - 处理任务: Task 2
应用场景(主要适用于延迟任务):