文章目录
- 队列结构图
- 队列种类划分
- 阻塞 / 非阻塞 :
- `阻塞队列`
- `非阻塞队列`
- 总结:
- 有界/无界
- `有界`
- `无界`
- 单向 / 双向链表
- `单向链表`
- `双向链表`
- Queue(队列)接口方法:
- 增删查(抛异常)
- 增删查(返回特殊值)
- Deque(双端队列)接口方法
- 增删查(抛异常)
- `First`Element (Head)
- `Last`Element (Tail)
- 增删查(返回特殊值)
- `First`Element (Head)
- `Last`Element (Tail)
- 队列使用
- 双端队列
- LinkedList
- ArrayDeque
- ConcurrentLinkedDeque
- 非阻塞队列
- PriorityQueue
- ConcurrentLinkedQueue
- 阻塞队列
- PriorityBlockingQueue(优先级阻塞队列)
- 使用:
- DelayQueue(延时队列)
- 原理
- 使用
- SynchronousQueue
- 功能说明:
队列是一种特殊的线性表,遵循先入先出、后入后出的基本原则,
一般来说,它只允许在表的前端进行删除操作,而在表的后端进行插入操作,
但是java的某些队列运行在任何地方插入删除;比如我们常用的 LinkedList 集合,它实现了Queue 接口,因此,我们可以理解为 LinkedList 就是一个队列;
队列结构图
队列种类划分
队列的种类划分可以分为三种:
- 是否阻塞?
- 有界还是无界
- 单向链表还是双向链表
阻塞 / 非阻塞 :
阻塞队列
- 入列(添加元素):
入列时, 如果元素数量超过队列总数 , 会进行等待(阻塞) , 等队列的中的元素出列后,
元素数量未超过队列总数时,就会解除阻塞状态,进而可以继续入列;- 出列(读取元素):
出列时,如果队列为空的情况下, 也会进行等待(阻塞) , 直到队列中入列了元素,即会接触阻塞状态- 好处 :
可以防止队列容器溢出
, 只要满了就会阻塞等待, 也就不存在溢出的情况
非阻塞队列
- 不管入列还是出列 , 都不会进行阻塞;
- 出列时 , 如果元素数量超出队列总数 , 则
会抛出异常
;- 出列时 , 如果队列为空,则取出空值;
总结:
- 只要是阻塞队列,都是
线程安全的
; - 一般情况下 , 非阻塞式队列使用的比较少 , 一般都是用阻塞式的队列比较多;
- 区分是否为阻塞队列的方法在于 看他是否有以下两个方法 :
- 出列阻塞方法: take( )
- 入列阻塞方法: put( )
有界/无界
有界
有界限,长度大小受限制
无界
- 无限大小 , 说是无限大小 , 其实是有限的 , 只不过超过界限时会进行扩容;
- 就像ArrayList一样在内部进行动态扩容
单向 / 双向链表
单向链表
每个元素中除了元素本身之外,还存储一个指针,这个指针指向下一个元素;
双向链表
除了元素本身之外,还有两个指针,一个指针指向前一个元素的地址,另一个指针指向后一个元素的地址
Queue(队列)接口方法:
队列提供了附加的
插入
、提取
和检查
操作。这些方法以两种形式存在:
- 一种在操作失败时抛出异常,
- 另一种返回特殊值(null或false,取决于操作)。在大多数实现中,插入操作不会失败。
增删查(抛异常)
方法名 | 作用 |
boolean add(E e) | 在尾部增加一个元素,成功返回true 如果队列已满,则抛出一个IIIegaISlabEepeplian异常 |
E remove() | 移除并返回 队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常 |
E element() | 返回队列的头部信息,但不删除 如果队列为空,则抛出一个NoSuchElementException异常 |
增删查(返回特殊值)
这种插入操作形式是专门为使用容量受限的Queue实现而设计的;
方法名 | 作用 |
boolean offer(E e) | 在尾部增加一个元素,成功返回true, 如果队列满了返回false 使用容量受限的队列时,这种方法通常更适合添加 |
E poll() | 检索并删除该队列的头部,如果该队列为空,则返回null。 |
E peek() | 检索但不删除该队列的头部,如果该队列为空,则返回null |
Deque(双端队列)接口方法
支持两端元素插入和移除的线性集合。 deque这个名字是“double ended queue(双端队列)”的缩写
这个接口定义了访问deque容器
两端元素
的方法。提供了插入、删除和检查元素的方法。
这些方法以两种形式存在:
- 一种在操作失败时
抛出异常
- 另一种
返回特殊值
(null或false,取决于操作)提供了对队列的头和尾操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXhThHBg-1668410534435)(java队列.assets/image-20220714174742689.png)]
增删查(抛异常)
FirstElement (Head)
方法名 | 作用 |
boolean addFirst(e) | 从头插入元素,如果没有空间,则抛出IllegalstateException异常 如果队列是有界的,最好使用offerFirst方法 |
E removeFirst() | 返回并删除头部第一个元素 , 如果没有 , 抛出异常 |
E getFirst() | 获取头部第一个元素 , 但是不删除 , 如果队列为空,抛出异常 |
LastElement (Tail)
方法名 | 作用 |
boolean addLast(e) | 从尾插入元素,如果没有空间,则抛出IllegalstateException异常 如果队列是有界的,最好使用offerLast方法 |
E removeLast() | 返回并删除尾部第一个元素 , 如果没有 , 抛出异常 |
E getLast() | 获取尾部第一个元素 , 但是不删除 , 如果队列为空,抛出异常 |
增删查(返回特殊值)
这种插入操作形式是专门为 使用容量受限的Deque实现而设计的;
FirstElement (Head)
方法名 | 作用 |
boolean offerFirst(e) | 从头插入一个元素 , 插入成功返回true , 插入失败返回 flase |
E pollFirst() | 返回并删除头部第一个元素 , 如果没有 , 返回null |
E peekFirst() | 获取头部第一个元素 , 但是不删除 , 如果没有 , 返回null |
LastElement (Tail)
方法名 | 作用 |
boolean offerLast(e) | 从尾插入一个元素 , 插入成功返回true , 插入失败返回false |
E pollLast() | 返回并删除尾部第一个元素 , 如果没有 , 返回null |
E peekLast() | 获取尾部第一个元素 , 但是不删除 , 如果没有 , 返回null |
总结:
Queue:
错误处理 | 抛出异常 | 返回特殊值 |
入队列 | add(e) | offer(e) |
出队列 | remove( ) | poll( ) |
队首元素 | element( ) | peek( ) |
Deque:
头插入 | 头插入 | 尾插入 | 尾插入 | |
抛出异常 | 返回特殊值 | 抛出异常 | 返回特殊值 | |
入队列 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
出队列 | removeFirst( ) | pollFirst( ) | removeLast( ) | pollLast( ) |
检索元素 | getFirst( ) | peekFirst( ) | getLast( ) | peekLast( ) |
双端队列(Deque):
LinkedList
ArrayDeque
ConcurrentlinkedDeque
阻塞队列:
DelayQueue 延迟队列
SynchronousQueue 同步队列
PriorityBlockingQueue 优先级队列
LinkedBlockingQueue 链表阻塞队列
ArrayBlockingQueue 数组阻塞队列
队列使用
双端队列
支持两端元素插入和移除的线性集合。 deque这个名字是“double ended queue(双端队列)”的缩写
LinkedList
双向链表 , 线程不安全 , 非阻塞 , 最多可以存入一个null
public static void main(String[] args) {
//创建双端队列
Deque<Integer> deque = new LinkedList<>();
dequeMethod(deque);
}
父类Queue的方法
//插入
deque.add(1); //在末尾插入一条数据,如果满了抛出异常
deque.offer(2); //在末尾插入一条数据 , 成功返回true , 失败返回false
//查询
Integer element = deque.element(); //返回头部信息但不删除 , 如果队列为空,报错
Integer peek = deque.peek(); //返回头部信息但不删除 , 如果队列尾空,返回null
//删除
Integer remove = deque.remove(); //返回并删除头部的数据 如果队列为空,报错
Integer poll = deque.poll(); //返回并删除 , 如果队列尾空 返回null
抛出异常的方法
//插入
deque.addFirst(1); //从头插入一个元素 , 如果没有空间 抛出异常
deque.addLast(2); //从尾插入一个元素 , 如果没有空间 抛出异常
//查询
Integer first = deque.getFirst(); //获取头部第一个元素 , 但是不删除 , 如果队列为空,抛出异常
Integer last = deque.getLast(); //获取尾部第一个元素 , 但是不删除 , 如果队列为空,抛出异常
//删除
Integer integer = deque.removeFirst(); //返回并删除头部第一个元素 , 如果没有 , 抛出异常
Integer integer1 = deque.removeLast(); //返回并删除尾部第一个元素 , 如果没有 , 抛出异常
返回特殊值的方法
//插入
deque.offerFirst(1); //从头插入一个元素 , 插入成功返回true , 插入失败返回 flase
deque.offerLast(2); //从尾插入一个元素 , 插入成功返回true , 插入失败返回false
//查询
Integer integer2 = deque.peekFirst(); //获取头部第一个元素 , 但是不删除 , 如果没有 , 返回null
Integer integer3 = deque.peekLast(); //获取尾部第一个元素 , 但是不删除 , 如果没有 , 返回null
//删除
Integer integer4 = deque.pollFirst(); //返回并删除头部第一个元素 , 如果没有 , 返回null
Integer integer5 = deque.pollLast(); //返回并删除尾部第一个元素 , 如果没有 , 返回null
ArrayDeque
- 无容量大小限制,容量按需增长;
- 非线程安全队列,无同步策略,不支持多线程安全访问
- 两端都可以操作
- 不能存储
null
操作和LinkedList 一样 , 就不演示了
ConcurrentLinkedDeque
是基于链表的无限双端队列,线程安全,不允许 null 元素。
内部通过 CAS 来实现线程同步,一般来说,如果需要使用
线程安全
的双端队列,那么推荐使用该类。操作和LinkedList 一样 , 就不演示了
非阻塞队列
PriorityQueue
- 动态增加 , 无线容量
- 非线程安全
- 不允许使用 null 元素
存入的值会根据排序给则进行排序
以下三类方法 :
descPriority(); 数字类型 自定义比较器 降序排序
defaultPriority(); 数字类型 , 默认升序排序
objectPriority(); 自定义类型 , 自定义比较器 , 升序排序
不使用比较器
, 默认升序
排序 在 Integer , Long 中可以使用
public static void defaultPriority(){
Queue<Integer> queue = new PriorityQueue<>();
//优先队列会将存入的数字类型元素,进行升序排序 如一下三个存入 2,1,3 会被排序为 => [1, 2, 3]
queue.add(2);
queue.add(1);
queue.add(3);
Integer remove = queue.peek();
System.out.println(remove);
System.out.println(queue);
}
自定义比较器
, 让Integer , Long 类型 进行降序
排序 如: 9,8,7,6,5,4,3,2,1重写 Comparator 类
public static void DescPriority(){
//!!!关键!!! 重写比较器
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; //降序
}
};
//在创建对象时将比较器放入参数中
Queue<Integer> queue = new PriorityQueue<>(comparator);
//优先队列会将存入的数字类型元素,进行升序排序 如一下三个存入 2,1,3 会被排序为 => [1, 2, 3]
queue.add(2);
queue.add(1);
queue.add(3);
Integer remove = queue.peek();
System.out.println(remove);
System.out.println(queue);
}
自定义类
, 进行比较 如: 比较Student类中的 id
public static void ObjectPriority(){
//重写比较器 : 先判断id进行升序 , 如果id相同判断年龄升序
Comparator<Student> comparator = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
if(o1.getId()!=o2.getId()) {
return o1.getId() - o2.getId();
}else{
return o1.getAge()-o2.getAge();
}
}
};
Student student = new Student();
//在创建对象时将比较器放入参数中
Queue<Student> queue = new PriorityQueue<>(comparator);
//优先队列会将存入的数字类型元素,进行升序排序 如一下三个存入 2,1,3 会被排序为 => [1, 2, 3]
queue.add(new Student(1,2,"张三"));
queue.add(new Student(2,5,"李四"));
queue.add(new Student(2,3,"王五"));
queue.add(new Student(1,2,"王五"));
System.out.println(queue);
}
ConcurrentLinkedQueue
基于链接节点的无界线程安全的队列,按照先进先出原则对元素进行排序。
新元素从队列尾部插入,而获取队列元素,则需要从队列头部获取。
阻塞队列
- 只要是阻塞队列,都是
线程安全的
;- 区分是否为阻塞队列的方法在于 看他是否有以下两个方法 :
- 出列阻塞方法: take( )
- 入列阻塞方法: put( )
PriorityBlockingQueue(优先级阻塞队列)
优先级阻塞队列, 是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;
它又是具有优先级的队列,是通过构造函数传入的对象来判断.
使用:
- 首先让传入的对象 实现(implements) Comparable<Object>
- 然后重写 compareTo(Object o) 方法 , 在里面写排序规则
package queue.blockingQueue;
import lombok.Data;
/**
* @Description: 实体类
*/
@Data
public class Student implements Comparable<Student>{
private Integer id ;
private Integer age ;
private String name;
/* !!!重要!!!
* 排序规则 , 重写Comparable类的方法
* int compareTo(T o)
* 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
如果返回值等于零: o1 = o2
返回值大于零则 o1 > o2
返回值小于于零则 o1 < o2
*/
@Override
public int compareTo(Student o2) {
// int i = this.age - o2.age; // 升序
int i = o2.age - this.age; // 降序
if(i>0){
return 1 ;
}else if (i<0){
return -1 ;
}else{
return 0 ;
}
}
}
package queue.blockingQueue;
import java.util.Arrays;
import java.util.concurrent.PriorityBlockingQueue;
/**
* @Author: Junlong.huang
* @Date: 2022/11/01 : 10:55
* @Description: 无界队列,它没有限制,在内存允许的情况下可以无限添加元素;
* 它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
*/
public class PriorityBlockingQueueDemo {
public static void main(String[] args) throws Exception {
//创建优先级阻塞对象
PriorityBlockingQueue<Student> p = new PriorityBlockingQueue<>();
//开一条线程 循环接收队列的内容
Thread thread = new Thread(() -> {
try {
//循环接收数据
while (true) {
//使用 .take(); 方法阻塞接收队列的数据 , 队列里有数据就会拿出来
Student take = p.take();
System.out.println(take);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
//使用 put() 往队列中塞对象
p.put(new Student(1, 2, "张三"));
Thread.sleep(1000 * 5);
p.put(new Student(2, 5, "李四"));
Thread.sleep(1000 * 5);
p.put(new Student(2, 3, "王五"));
Thread.sleep(1000 * 5);
p.put(new Student(1, 2, "王五"));
}
}
DelayQueue(延时队列)
延时队列, 是一个通过
PriorityBlockingQueue
实现延迟获取元素的无界阻塞
队列;其中添加进该队列的元素必须实现Delayed接口(指定延迟时间),而且只有在延迟期满后才能从中提取元素。
延时
队列
相比于普通队列最大的区别就体现在其延时的属性上,普通队列的元素是先进先出,按入队顺序进行处理,
而延时队列中的元素在入队时会指定一个延迟时间,
表示其希望能够在经过该指定时间后处理。从某种意义上来讲,延迟队列的结构并不像一个队列,
而更像是一种以时间为权重的有序堆结构。
原理
DelayQueue的泛型参数需要实现 Delayed 接口,Delayed接口继承了 Comparable 接口,
DelayQueue内部使用
非线程安全的优先队列
(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。DelayQueue不允许包含null元素。
使用
使用延迟队列 , 时需要 实现(implements) Delayed
delayed 接口 继承(extends) Comparable<Delayed>
其中:
返回与此对象关联的剩余延迟(给定的时间单位)
剩余时间 = 到期时间 - 当前时间
unit 时间单位
返回剩余延迟 零值或负值表示延迟已过期
@Override
public long getDelay(TimeUnit unit) {
// 剩余时间 = 到期时间 - 当前时间
long residueTime = this.expire - System.currentTimeMillis();
//将剩余时间 转为毫秒
long convert = unit.convert(residueTime, TimeUnit.MILLISECONDS);
return convert;
}//优先级规则:两个任务比较,时间短的优先执行
@Override
public int compareTo(Delayed o) {
long l = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
return (int)l;
}
创建 实体类
package queue.blockingQueue;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @Author: Junlong.huang
* @Date: 2022/11/01 : 15:40
* @Description:
*/
@Data
public class User implements Delayed {
private String name ;
private Integer age;
long delayTime; // 延迟时间
long expire; // 过期时间
public User(long delayTime , String name , Integer age){
this.name=name;
this.age=age;
this.delayTime = delayTime;
// 过期时间 = 当前时间 + 延迟时间
this.expire = System.currentTimeMillis() + delayTime;
}
/**
* 返回与此对象关联的剩余延迟(给定的时间单位)。
* @param unit 时间单位
* @return 返回剩余延迟 零值或负值表示延迟已过期
* 剩余时间 = 到期时间 - 当前时间
*/
@Override
public long getDelay(TimeUnit unit) {
// 剩余时间 = 到期时间 - 当前时间
long residueTime = this.expire - System.currentTimeMillis();
//将剩余时间 转为毫秒
long convert = unit.convert(residueTime, TimeUnit.MILLISECONDS);
return convert;
}
/**
* 优先级规则:两个任务比较,时间短的优先执行
*/
@Override
public int compareTo(Delayed o) {
long l = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
return (int)l;
}
}
使用
package queue.blockingQueue;
import java.util.Queue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @Description: 延迟队列
*/
public class DelayQueueDemo {
//使用队列方法
public static void delayDemo() throws Exception {
DelayQueue<Delayed> delayeds = new DelayQueue<>();
//使用put进行添加 put是线程安全的
//TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES) 表示将 2 分钟 转为 对应的毫秒数
delayeds.put(new User( TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES) , "1", 1));
delayeds.put(new User( TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES) , "2", 1));
delayeds.put(new User( TimeUnit.MILLISECONDS.convert(4, TimeUnit.MINUTES) , "3", 1));
delayeds.put(new User( TimeUnit.MILLISECONDS.convert(3, TimeUnit.MINUTES) , "4", 1));
while (true) {
Delayed take = delayeds.take();
System.out.println(take);
}
}
public static void main(String[] args) throws Exception {
delayDemo();
}
}
SynchronousQueue
同步队列 , 这是一个比较特殊的队列 , 可用于
线程间交换数据
却不用存储数据
。使用SynchronousQueue可以在两个线程中传递同一个对象。一个线程放对象,另外一个线程取对象。
功能说明:
再之前的学习中 , 如果线程间要交换数据一般都是用一个通过公共变量或者一个同步阻塞队列,生产者线程设置变量或者往队列中put值,消费者线程则读取变量或者从队列中take。
而SynchronousQueue则不需要存储线程间交换的数据,它的作用更像是一个匹配器,使生产者和消费者一一匹配。
比如当一个线程调用了put方法时,发现队列中没有take线程,那么put线程就会阻塞,当take线程进来时发现有阻塞的put线程,那么他们两个就会匹配上,然后take线程获取到put线程的数据,两个线程都不阻塞。
反之一个线程调用take方法也会阻塞线程,当一个调用put方法的线程进来后也会与之匹配。
如果一个take或者put线程进来发现有同类的take或者put线程在阻塞中,那么线程会排到后面,直到有不同类的线程进来然后匹配其中一个线程。
public static void main(String[] args) {
SynchronousQueue<Integer> s = new SynchronousQueue<>();
AtomicInteger a = new AtomicInteger(1);
//从队列中获取数据
Thread t = new Thread(()->{
while (true){
try {
Integer take = s.take();
System.out.println("获取值为:"+take);
} catch (InterruptedException e) { e.printStackTrace(); }
}
});
//开一个线程,将数据存入线程中
Thread t1 = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
s.put(a.getAndIncrement());
} catch (InterruptedException e) { e.printStackTrace(); }
}
});
//开一个线程,将数据存入线程中
Thread t2 = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
s.put(a.getAndIncrement());
} catch (InterruptedException e) { e.printStackTrace(); }
}
});
t.start();
t1.start();
t2.start();
}