AQS系列
1、AQS核心原理
2、ReentrantLock 示例及原理
3、CountDownLatch / Semaphore 示例及使用场景
4、BlockingQueue 示例及使用场景
5、静态代码块中使用 ExecutorService 执行多线程会出现什么情况呢?
一、概述
我们在日常开发中会经常碰到这样的场景,总是感觉某个程序去处理数据或者完成某个数据推送的时候有点慢,那就让多个线程去执行呗,可有时候处理逻辑是有顺序的,比如先更新主表在更新从表,或者是多个任务之间有依赖关系,先执行任务1再执行任务2,这种情况下我们就可以使用 BlockingQueue。
BlockingQueue 是 juc包提供的一个用于解决并发生产者 - 消费者之间数据传递的最有用的类,在任何时候只有一个线程进行 take / pull 或者 put 操作,而且 BlockingQueue 提供了超时时 return null 的机制,以供我们很好的在多线程中进行数据传递操作,提高效率。
二、基本原理
BlockingQueue 是一个阻塞队列,队列分为以下两种类型:
- 无限队列(unbounded queue):几乎可以无限增长;
- 有限队列(bounded queue):定义最大的容量;
队列其实也是一种用于存储数据的集合,就像 List 那样,可以往一个队列中 put 好多数据,队列具有以下特性:
- 通常用数据或者链表的方式实现;
- 一般队列都是 FIFO(先进先出)的特性,就像JVM内存的栈,也有双端队列(Deque)优先级队列;
- 队列主要两种操作,入队 和 出队;
日常工作中常见的队列有以下几种:
- ArrayBlockingQueue:有数组支持的有界队列;
- LinkedBlockingQueue:由链表节点支持的有界或无解队列;
- PriorityBlockingQueue: 优先级堆支持的无界优先级队列;
- DelayQueue:由优先级堆支持的基于时间的调度队列;
三、示例
3.1 ArrayBlockingQueue
这里有一个很简单的例子,写的比较复杂反而看着费劲,先实例化一个队列 idQueue 阻塞队列出来,用于有顺序的存放 id,第一个线程是死循环从队列中获取值,第二个线程只循环 5 次往队列中放值,代码如下:
public class ArrayBlockingQueueTest {
public static BlockingQueue<Integer> idQueue = new ArrayBlockingQueue(10);
public static void main(String[] args) {
//这里先有一个线程从队列中获取值
new Thread(() -> {
for(;;){
try {
Integer value = idQueue.take();
System.out.println("从队列中获取到值:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//这里有一个线程每隔 1 秒往队列中放
new Thread(() -> {
for(int i=0; i < 5; i++){
try {
//往队列中插入
idQueue.put(i);
System.out.println("往队列中放入值:" + i);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
我们来看看执行结果:
生产者往队列中放入值:0
消费者从队列中获取到值:0
生产者往队列中放入值:1
消费者从队列中获取到值:1
消费者从队列中获取到值:2
生产者往队列中放入值:2
生产者往队列中放入值:3
消费者从队列中获取到值:3
生产者往队列中放入值:4
消费者从队列中获取到值:4
(阻塞中......)
从结果可以看出,每当生成者向阻塞队列中放入一个值,消费者线程就会立马消费这个值,当生产者循环5次完了之后,就不会在往队列中放值了,那么消费者线程就阻塞到这个地方等待了,这是 put() 和 take() 方法。
方法总结如下:
添加元素
方法 | 说明 |
---|---|
offer() | 如果插入元素成功则返回 true, 否则返回 false |
offer(E e, long timeout, TimeUnit unit) | 尝试将元素插入队列,如果队列满了,则等待,等待时间超过了指定时间则抛出中断异常 |
add() | 如果插入元素成果则返回 true, 否在抛出 IllegalStateException 异常, 其实 add() 方法底层调用了 offer() 方法 |
put() | 将元素插入队列,如果队列满了,则阻塞直到队列有空间执行插入操作 |
取出元素
方法 | 说明 |
---|---|
take() | 获取队列头元素并将其删除,如果队列为空了则阻塞等待元素被添加进来 |
poll() | 回去队列头元素并将其删除,如果队列为空则返回 null |
poll(long timeout, TimeUnit unit) | 获取队列头的元素并将其删除,如果队列为空,则等待,等待元素被插入,等待超时了,则返回 null |
3.2 PriorityBlockingQueue
下面的例子中,随机生成了5个随机数放到队列中,然后获取这些数。
我们首先要新建一个类实现 Comparable 接口,复写 compareTo 方法就有了排序功能。
public class PriorityBlockingQueueTest {
public static void main(String[] args) {
BlockingQueue<PriorityItem> queue = new PriorityBlockingQueue();
Random random = new Random();
System.out.println("先随机生成5个数放入队列!");
for (int i = 0; i < 5; i++) {
int i1 = random.nextInt(10);
queue.add(new PriorityItem(i1));
System.out.println("Producer : " + i1);
}
System.out.println("从队列中获取值!");
for(;;){
try {
System.out.println("Consumer : " + queue.take().getIndex());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class PriorityItem implements Comparable {
private int index;
public PriorityItem(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
@Override
public int compareTo(Object o) {
PriorityItem item = (PriorityItem) o;
if(this.getIndex() > item.getIndex()){
return 1;
}else if(this.getIndex() < item.getIndex()){
return -1;
}
return 0;
}
}
}
执行结果:
先随机生成5个数放入队列!
Producer : 1
Producer : 0
Producer : 6
Producer : 0
Producer : 6
从队列中获取值!
Consumer : 0
Consumer : 0
Consumer : 1
Consumer : 6
Consumer : 6
(阻塞中.....)
从打印结果中看到,放到队列中的值是按顺序打印的。
我和 ChatGPT 讨论了这个优先级队列,下面是ChatGPT解释的,比较完美:
PriorityBlockingQueue 是优先级队列,如果我们只是在生产者线程中随机生成数字并立即将其放入优先级队列中,那么在消费者线程中获取这些数字时,它们并不会按照从小到大的顺序排列。这是因为,在优先级队列中,只有在调用take()或poll()方法时,才会对元素进行排序。
PriorityBlockingQueue 这个优先队列类似于有序集合,它能够确保元素在队列中始终是有序的。如果生产者不断向队列中放入元素,而消费者也不断从队列中取出元素,那么队列中的元素的顺序是不会发生变化的,除非消费者的速度比生产者慢,从而导致元素在队列中被暂时存储起来,等待消费者来取出。
因此,PriorityBlockingQueue 更适合用于生产者和消费者的生产和消费速度有差异的情况。在这种情况下,如果生产者的速度比消费者快,那么队列中的元素会被暂时存储起来,等待消费者来取出;如果消费者的速度比生产者快,那么消费者会等待生产者放入新的元素。
总之,PriorityBlockingQueue 适用于需要在多线程环境下保持元素有序的场景,但如果生产者和消费者的速度相同,则它的排序功能并不会发挥作用。
3.4 DelayQueue
下面的例子中,我们首先新建一个 DelayItem类实现了Delayed接口,并且复写了 getDelay() 方法 和 compareTo() 方法,getDealy() 是控制过期时间的,compareTo() 方法是用于比较元素加入队列的优先级的,上面讲过,DelayQueue是一个有优先级的基于时间调度的队列。
public class DelayQueueTest {
public static void main(String[] args) {
BlockingQueue<DelayItem> delayQueue = new DelayQueue<>();
delayQueue.add(new DelayItem(5, 1000));
delayQueue.add(new DelayItem(6, 2000));
delayQueue.add(new DelayItem(3, 3000));
delayQueue.add(new DelayItem(2, 4000));
delayQueue.add(new DelayItem(7, 5000));
delayQueue.add(new DelayItem(8, 3000));
delayQueue.add(new DelayItem(10, 4000));
System.out.println("poll():" + delayQueue.poll());
System.out.println("peek():" + delayQueue.peek().getValue());
try {
for(;;){
System.out.println("take():" + delayQueue.take().getValue());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class DelayItem implements Delayed{
private long expireTime;
private int value;
public DelayItem(int value, long expireTime) {
this.value = value;
this.expireTime = System.currentTimeMillis() + expireTime;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
DelayItem delayItem = (DelayItem) o;
if(delayItem.getValue() % 2 == 0){
return 1;
}else if(delayItem.getValue() < 0){
return -1;
}
return 0;
}
public int getValue() {
return value;
}
}
}
执行结果:
poll():null
peek():5
take():5
take():6
take():2
take():10
take():8
take():3
take():7
(阻塞中.....)
从运行结果看出:poll() 方法获取到了null, 因为队列中的元素还未过期,其他方法如下:
方法 | 说明 |
---|---|
poll() | 立即返回元素,并删除元素,如果没有过期的元素则返回 null |
peek() | 立即获取队列头元素,不会删除元素,如果没有过期的元素 |
take() | 进行阻塞,等元素过期在获取元素,如果没有则继续阻塞 |