队列是什么
队列用于模拟队列这种数据结构,队列通常是指“先进先出”的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素
Queue的6个方法
压入元素(添加):add()、offer()
相同:未超出容量,从队尾压入元素,返回压入的那个元素。
区别:在超出容量时,add()方法会对抛出异常,offer()返回false
弹出元素(删除):remove()、poll()
相同:容量大于0的时候,删除并返回队头被删除的那个元素。
区别:在容量为0的时候,remove()会抛出异常,poll()返回false
获取队头元素(不删除):element()、peek()
相同:容量大于0的时候,都返回队头元素。但是不删除。
区别:容量为0的时候,element()会抛出异常,peek()返回null。
队列的UML类图
Queue的分类
阻塞队列
阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。
public class BlockingQueue {
public static void main(final String[] args) {
final ArrayBlockingQueue queue = new ArrayBlockingQueue(5);
Thread thread1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + " | ArrayBlockingQueue Size:" + queue.size());
}
System.out.println(new Date() + " | For End.");
}
};
thread1.start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!queue.isEmpty()){
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
非阻塞队列
非阻塞队列也就是普通队列,它的名字中不会包含 BlockingQueue 关键字,并且它不会包含 put 和 take 方法,当队列满之后如果还有新元素入列会直接返回错误,并不会阻塞的等待着添加元素,如下图所示:
非阻塞队列的典型代表是 ConcurrentLinkedQueue 和PriorityQueue。
有界队列
有界队列:是指有固定大小的队列,比如设定了固定大小的 ArrayBlockingQueue,又或者大小为 0 的 SynchronousQueue。
无界队列
无界队列:指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是 Integer.MAX_VALUE,当然实际的使用中不会有这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的角度来看相当于 “无界”的。
普通队列
普通队列(Queue)是指实现了先进先出的基本队列,例如 ArrayBlockingQueue 和 LinkedBlockingQueue,其中
ArrayBlockingQueue是用数组实现的普通队列,如下图所示
而 LinkedBlockingQueue 是使用链表实现的普通队列,如下图所示:
注意:一般情况下 offer() 和 poll() 方法配合使用,put() 和 take() 阻塞方法配合使用,add() 和 remove() 方法会配合使用,程序中常用的是 offer() 和 poll() 方法,因此这两个方法比较友好,不会报错。
public class BlockingQueue {
public static void main(final String[] args) {
LinkedBlockingQueue queue = new LinkedBlockingQueue(2);
queue.offer("Hello");
queue.offer("Java");
queue.offer("中文社群");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
双端队列
双端队列(Deque)是指队列的头部和尾部都可以同时入队和出队的数据结构,如下图所示:
java.util.concurrent 包里的 BlockingDeque 接口表示一个线程安放入和提取实例的双端队列。
BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。 deque(双端队列) 是 “Double Ended Queue” 的缩写。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列。
在线程既是一个队列的生产者又是这个队列的消费者的时候可以使用到 BlockingDeque。如果生产者线程需要在队列的两端都可以插入数据,消费者线程需要在队列的两端都可以移除数据,这个时候也可以使用 BlockingDeque。BlockingDeque 图解:
BlockingDeque 的方法
一个 BlockingDeque - 线程在双端队列的两端都可以插入和提取元素。 一个线程生产元素,并把它们插入到队列的任意一端。如果双端队列已满,插入线程将被阻塞,直到一个移除线程从该队列中移出了一个元素。如果双端队列为空,移除线程将被阻塞,直到一个插入线程向该队列插入了一个新元素。
public class BlockingQueue {
public static void main(final 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
优先队列
优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队,内部基于数组实现,线程不安全的队列
使用优先队列时应规定排序规则(需要在类里实现Comparable接口,或自己写一个实现Comparator接口的类)
public class BlockingQueue {
public static void main(final String[] args) {
PriorityQueue queue = new PriorityQueue<>(10,
new Comparator<Student>(){
@Override
public int compare(Student v1, Student v2) {
//按照level倒序取出
return v2.getLevel() - v1.getLevel();
}
});
queue.offer(new Student(1,"a",5));
queue.offer(new Student(1,"a",1));
queue.offer(new Student(1,"a",3));
while(!queue.isEmpty()) {
System.out.println(queue.poll().toString());
}
}
@Data
private static class Student {
private int id;
private String name;
private int level;
public Student(int id, String name, int level) {
this.id = id;
this.name = name;
this.level = level;
}
}
}
BlockingQueue.Student(id=1, name=a, level=5)
BlockingQueue.Student(id=1, name=a, level=3)
BlockingQueue.Student(id=1, name=a, level=1)
其他队列
在 Java 的队列中有一个比较特殊的队列 SynchronousQueue,它的特别之处在于它内部没有容器,每次进行 put() 数据后(添加数据),必须等待另一个线程拿走数据后才可以再次添加数据,它的使用示例如下:
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();
}
}
Wed Oct 20 01:54:52 CST 2021,元素入队
Wed Oct 20 01:54:53 CST 2021,元素出队:Data 0
Wed Oct 20 01:54:53 CST 2021,元素入队
Wed Oct 20 01:54:54 CST 2021,元素出队:Data 1
Wed Oct 20 01:54:54 CST 2021,元素入队
Wed Oct 20 01:54:55 CST 2021,元素出队:Data 2
从上述结果可以看出,当有一个元素入队之后,只有等到另一个线程将元素出队之后,新的元素才能再次入队。
延迟队列
延迟队列(DelayQueue)是基于优先队列 PriorityQueue 实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。
Queue使用场景
很典型的JDK自带的线程池中就大量使用了Queue来存储任务。