队列的定义
队列是一种特殊的线性表,遵循的原则就是“先入先出”。在我们日常使用中,经常会用来并发操作数据。在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列通常有两种方式:一种是使用阻塞队列,另一种是使用线程同步锁。
队列的继承关系图
![](https://i-blog.csdnimg.cn/blog_migrate/41af77d5d793f5d9288714d69f3eeb26.png)
队列的概述
队列是一种重要的抽象数据结构,可类比于生活中的排队场景Java语言提供了队列的支持,内置了多种类型的队列供我们使用,队列数据存储显著特点就是先进先出,类似列车进涵洞。在并发场景,电商秒杀、队列的用处很大。
队列(Queue):队列简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队或进队;删除元素称为出队或离队。
这和我们日常生活中的排队是一致的,最早排队的也是最早离队的。其操作的特性是先进先出 (First In First Out, FIFO),故又称为先进先出的线性表。
![](https://i-blog.csdnimg.cn/blog_migrate/9b89fcde2573b4fec755f5d3c149749b.png)
队头(Front):允许删除的一端,又称为队首。
队尾(Rear):允许插入的一端。
空队列:不含任何元素的空表。
队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q, &x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q, &x):读队头元素,若队列Q非空,则将队头元素赋值给X。
我们常用的 LinkedList 集合,它实现了Queue 接口,因此,我们可以理解为 LinkedList 就是一个队列。
需要注意的是,队列是操作受限的线性表,所以,不是任何对线性表的操作都可以作为队列的操作。比如,不可以随便读取队列中间的某个数据。
队列的使用场景
消息队列是用来解决这样的问题的:将突发的大量请求转换位服务器能够处理的队列请求。eg:在一个秒杀活动中,服务器1秒可以处理100条请求。而在秒杀活动开启1秒进来1000个请求并且持续10秒。这个时候就需要将这10000个请求放入消息队列里面,后端按照原来的能力处理,用100秒将队列中的请求处理完毕。这样就不会宕机。
java队列特性
队列主要分为阻塞和非阻塞,有界和无界、单向链表和双向链表之分;
![](https://i-blog.csdnimg.cn/blog_migrate/618409b2cbdc17a8b1f79f90d5725ba1.png)
阻塞和非阻塞
阻塞队列
入列(添加元素)时,如果元素数量超过队列总数,会进行等待(阻塞),待队列的中的元素出列后,元素数量未超过队列总数时,就会解除阻塞状态,进而可以继续入列;
出列(删除元素)时,如果队列为空的情况下,也会进行等待(阻塞),待队列有值的时候即会解除阻塞状态,进而继续出列;
阻塞队列的好处是可以防止队列容器溢出;只要满了就会进行阻塞等待;也就不存在溢出的情况;
只要是阻塞队列,都是线程安全的;
非阻塞队列
不管出列还是入列,都不会进行阻塞,
入列时,如果元素数量超过队列总数,则会抛出异常,
出列时,如果队列为空,则取出空值;
一般情况下,非阻塞式队列使用的比较少,一般都用阻塞式的对象比较多;阻塞和非阻塞队列在使用上的最大区别就是阻塞队列提供了以下2个方法:
出队阻塞方法 : take()
入队阻塞方法 : put()
有界和无界
有界:有界限,大小长度受限制
无界:无限大小,其实说是无限大小,其实是有界限的,只不过超过界限时就会进行扩容,就行ArrayList 一样,在内部动态扩容单向链表和双向链表
单向链表 : 每个元素中除了元素本身之外,还存储一个指针,这个指针指向下一个元素;
![](https://i-blog.csdnimg.cn/blog_migrate/1af8a31c04d428161aaff509e9eeb0ae.png)
**双向链表 :**除了元素本身之外,还有两个指针,一个指针指向前一个元素的地址,另一个指针指向后一个元素的地址;
![](https://i-blog.csdnimg.cn/blog_migrate/6ba0c7b658ff8ed8a7627f71e0a16b7c.png)
队列常用方法
方法名 | 说明 |
add | 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常 |
remove | 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常 |
element | 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常 |
offer | 添加一个元素并返回true 如果队列已满,则返回false |
poll | 移除并返问队列头部的元素 如果队列为空,则返回null |
peek | 返回队列头部的元素 如果队列为空,则返回null |
put | 添加一个元素 如果队列满,则阻塞 |
take | 移除并返回队列头部的元素 如果队列为空,则阻塞 |
drainTo(list) | 一次性取出队列所有元素 |
java中队列的实现类
非阻塞队列
ConcurrentLinkedQueue
单向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全,内部基于节点实现
ConcurrentLinkedDeque
双向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全
PriorityQueue
内部基于数组实现,线程不安全的队列
阻塞队列
DelayQueue
一个支持延时获取元素的无界阻塞队列
LinkedTransferQueue
一个由链表结构组成的无界阻塞队列。
ArrayBlockingQueue
有界队列,阻塞式,初始化时必须指定队列大小,且不可改变;,底层由数组实现;
SynchronousQueue
最多只能存储一个元素,每一个put操作必须等待一个take操作,否则不能继续添加元素
PriorityBlockingQueue
一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,而且它也是无界的,也就是没有容量上限,虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError 错误
阻塞队列案例
阻塞队列(Blocking Queue)提供了可阻塞的put和take方法,它们与可定时的offer和pull是等价的。如果队列满了put方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么take方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put方法和take方法就永远不会阻塞。
![](https://i-blog.csdnimg.cn/blog_migrate/4755d088c0861b18a022394d833e7145.png)
我们可以从队列的名称中知道此队列是否为阻塞队列,阻塞队列中包含BlockingQueue关键字,比如以下:
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
阻塞功能队列演示:
public class BlockingTest {
public static void main(String[] args) throws InterruptedException {
// 创建一个长度为 5 的阻塞队列
ArrayBlockingQueue q1 = new ArrayBlockingQueue(5);
// 新创建一个线程执行入列
new Thread(() -> {
// 循环 10 次
for (int i = 0; i < 10; i++) {
try {
q1.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + " | ArrayBlockingQueue Size:" + q1.size());
}
System.out.println(new Date() + " | For End.");
}).start();
// 新创建一个线程执行出列
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
// 休眠 1S
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!q1.isEmpty()) {
try {
q1.take(); // 出列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
执行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/47e91ef34d4eaa6a00196c8621ba7fe1.png)
从上述结果可以看出,当==ArrayBlockingQueue== 队列满了之后就会进入阻塞,当过了 1 秒有元素从队列中移除之后,才会将新的元素入列。
非阻塞列
非阻塞队列也就是普通队列,它的名字不会包含BlockingQueue关键字,并且它不会包含put和take方法,当队列满之后如果还有新元素入列会直接返回错误,并不会阻塞的等待着添加元素。
![](https://i-blog.csdnimg.cn/blog_migrate/d6882b8e40090b9c5a02ee6aa9ee2e25.png)
非阻塞队列的典型代表是 ConcurrentLinkedQueue和 PriorityQueue。
有界队列和无界队列
有界队列:是指有固定大小的队列,比如设定了固定大小的ArrayBlockingQueue,又或者大小为0的SychronousQueue。
![](https://i-blog.csdnimg.cn/blog_migrate/fe099b04d622feb5ce80af1bfb459575.png)
无界队列:指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是Integer.MAX_VALUE,当然实际的使用中不会有这么大的容量(超过Integer.MAX_VALUE),所以从使用者的角度来看相当于”无界“的。
![](https://i-blog.csdnimg.cn/blog_migrate/d9dc133de7247070d97666cac04fc4ef.png)
按功能分类
我们以功能来划分一下队列,它可以被分为:普通队列、优先队列、双端队列、延迟队列、其他队列等,接下来我们分别来看。
1.普通队列
普通队列(Queue)是指实现了先进先出的基本队列,例如 ArrayBlockingQueue 和 LinkedBlockingQueue,其中 ArrayBlockingQueue是用数组实现的普通队列,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/961d0e92120f98e60bce8a2724775829.png)
而 LinkedBlockingQueue 是使用链表实现的普通队列,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/cc24a79763a2bbd488c69b7ae4477497.png)
LinkedBlockingQueue演示普通队列的使用:
import java.util.concurrent.LinkedBlockingQueue;
static class LinkedBlockingQueueTest {
public static void main(String[] args) {
LinkedBlockingQueue queue = new LinkedBlockingQueue();
queue.offer("Hello");
queue.offer("Java");
queue.offer("world");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
/***
结果
Hello
java
world
*/
2.双端队列
双端队列(Deque)是指队列的头部和尾部都可以同时入队和出队的数据结构,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/912e9a26206ec342ca343cedcfee79e4.png)
接下来我们来演示一下双端队列 LinkedBlockingDeque 的使用:
//双端对列
static class LinkedBlockingDequeTest {
public static void main(String[] args) {
// 创建一个双端队列
LinkedBlockingDeque deque = new LinkedBlockingDeque();
deque.offer("offer"); // 插入首个元素
deque.offerFirst("offerFirst"); // 队头插入元素
deque.offerLast("offerLast"); // 队尾插入元素
while (!deque.isEmpty()) {
// 从头遍历打印
System.out.println(deque.poll());
}
}
}
/***
结果
offerFirst
offer
offerLast
*/
3.优先对列
优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队。
优先队列是根据二叉堆实现的,二叉堆的数据结构如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/5838188bd72f19ec2553726d4a04a386.png)
两种类型:一种是最大堆一种是最小堆。以上展示的是最大堆,在最大堆中,任意一个父节点的值都大于等于它左右子节点的值。因为优先队列是基于二叉堆实现的,因此它可以将优先级最好的元素先出队。
优先队列的使用:
import java.util.PriorityQueue;
public class PriorityQueueTest {
// 自定义的实体类
static class Viper {
private int id; // id
private String name; // 名称
private int level; // 等级
public Viper(int id, String name, int level) {
this.id = id;
this.name = name;
this.level = level;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue(10, new Comparator<Viper>() {
@Override
public int compare(Viper v1, Viper v2) {
// 设置优先级规则(倒序,等级越高权限越大)
return v2.getLevel() - v1.getLevel();
}
});
// 构建实体类
Viper v1 = new Viper(1, "Java", 1);
Viper v2 = new Viper(2, "MySQL", 5);
Viper v3 = new Viper(3, "Redis", 3);
// 入列
queue.offer(v1);
queue.offer(v2);
queue.offer(v3);
while (!queue.isEmpty()) {
// 遍历名称
Viper item = (Viper) queue.poll();
System.out.println("Name:" + item.getName() +
" Level:" + item.getLevel());
}
}
}
/***
结果
Name:MySQL Level:5
Name:Redis Level:3
Name:Java Level:1
*/
4.延迟对列
延迟队列(DelayQueue)是基于优先队列 PriorityQueue 实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。
![](https://i-blog.csdnimg.cn/blog_migrate/6d5943b79a7b5423d3b0ea06d8032282.png)
代码演示:
public class CustomDelayQueue {
// 延迟消息队列
private static DelayQueue delayQueue = new DelayQueue();
public static void main(String[] args) throws InterruptedException {
producer(); // 调用生产者
consumer(); // 调用消费者
}
// 生产者
public static void producer() {
// 添加消息
delayQueue.put(new MyDelay(1000, "消息1"));
delayQueue.put(new MyDelay(3000, "消息2"));
}
// 消费者
public static void consumer() throws InterruptedException {
System.out.println("开始执行时间:" +
DateFormat.getDateTimeInstance().format(new Date()));
while (!delayQueue.isEmpty()) {
System.out.println(delayQueue.take());
}
System.out.println("结束执行时间:" +
DateFormat.getDateTimeInstance().format(new Date()));
}
static class MyDelay implements Delayed {
// 延迟截止时间(单位:毫秒)
long delayTime = System.currentTimeMillis();
// 借助 lombok 实现
@Getter
@Setter
private String msg;
/**
* 初始化
* @param delayTime 设置延迟执行时间
* @param msg 执行的消息
*/
public MyDelay(long delayTime, String msg) {
this.delayTime = (this.delayTime + delayTime);
this.msg = msg;
}
// 获取剩余时间
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// 队列里元素的排序依据
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return this.msg;
}
}
/**
结果:
开始执行时间:20223-02-28 20:17:28
消息1
消息2
结束执行时间:20223-02-28 20:17:31
**/
从上述结束执行时间和开始执行时间可以看出,消息 1 和消息 2 都正常实现了延迟执行的功能 。
5.其他对列
在 Java 的队列中有一个比较特殊的队列 SynchronousQueue,它的特别之处在于它内部没有容器,每次进行 put() 数据后(添加数据),必须等待另一个线程拿走数据后才可以再次添加数据,它的使用示例如下:
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueTest {
public static void main(String[] args) {
SynchronousQueue queue = new SynchronousQueue();
// 入队
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
System.out.println(new Date() + ",元素入队");
queue.put("Data " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 出队
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
System.out.println(new Date() + ",元素出队:" + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/***
Tue Feb 28 17:53:40 CST 2023,元素入队
Tue Feb 28 17:53:41 CST 2023,元素入队
Tue Feb 28 17:53:41 CST 2023,元素出队:Data 0
Tue Feb 28 17:53:42 CST 2023,元素出队:Data 1
Tue Feb 28 17:53:42 CST 2023,元素入队
Tue Feb 28 17:53:43 CST 2023,元素出队:Data 2
*/
从上述结果可以看出,当有一个元素入队之后,只有等到另一个线程将元素出队之后,新的元素才能再次入队。
参考链接
作者:Java中文社群