队列(Queue)也是「线性表」的一种,它的特点是:先进先出,表的一端只能添加元素,另一端只能删除元素。
添加元素叫作「入队」,删除元素叫作「出队」。入队一端叫作「队尾」,出队的一端叫作「队头」。
队列就和生活中的排队一样,得分个先来后到,大家不能插队,永远是先到的人先办事,后到的人后办事。
1. 队列的实现
和栈一样,队列既可以用数组实现,也可以用链表实现。
1.1 数组存储
使用数组来实现队列比较麻烦,数组的一端用来生产,另一端用来消费,但是有几个问题需要解决:
1、生产者到达数组末端,但是数组首端已经被消费了部分数据,这部分空间如何处理?
生产者应该回到数组首端,利用这部分已经被消费的空间,形成一个循环队列。
2、即使是循环队列,数组满仍要扩容。
【分析】
初始化一个数组,使用变量front和back分别指向队头和队尾。不管是出队还是入队,指针到达数组末端后都要自动回到首端,形成循环队列。变量size保存元素的个数,数组被填满后自动扩容。
【实现】
public class ArrayQueue<T> {
private Object[] table;
private int size;
private int front;//队头
private int back;//队尾
public ArrayQueue() {
this.table = new Object[16];
}
public void add(T data) {
if ((back + 1 - front) % table.length == 0) {
// 扩容
int oldFront = front;
front = 0;
back = table.length - 1;
Object[] newTable = new Object[table.length << 1];
for (int i = 0; i < table.length; i++) {
newTable[i] = table[oldFront];
oldFront = (oldFront + 1) % table.length;
}
table = newTable;
}
size++;
back = (back + 1) % table.length;
table[back] = data;
}
public T poll() {
if (size == 0) {
throw new RuntimeException("Queue is empty");
}
size--;
front = (front + 1) % table.length;
return (T) table[front];
}
public static void main(String[] args) {
ArrayQueue<Integer> queue = new ArrayQueue<>();
for (int i = 1; i <= 10; i++) {
queue.add(i);
}
for (int i = 0; i < 5; i++) {
System.out.println(queue.poll());
}
for (int i = 11; i <= 50; i++) {
queue.add(i);
}
for (int i = 0; i < 45; i++) {
System.out.println(queue.poll());
}
}
}
1.2 链式存储
使用链表来实现数组就非常简单了,直接操作链头链元素即可,也不用担心扩容的问题。
【分析】
初始化一个空链表,入队时尾插法将数据封装成Node节点入队,出队时将链头元素从链表中移除并返回。
【实现】
public class LinkedQueue<T> {
private int size;
private Node<T> front;//队头
private Node<T> back;//队尾
public void add(T data) {
Node<T> node = new Node<>(data, null);
if (size++ == 0) {
front = back = node;
} else {
// 尾插法
back.next = node;
back = node;
}
}
public T poll() {
if (size == 0) {
throw new RuntimeException("Queue is empty");
}
size--;
T data = front.data;
front = front.next;
return data;
}
private class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
}
2. 队列的应用
队列的特点是「先进先出」,它确保先入队的元素有更高的优先级,因此特别适合用来做排队类的场景。例如:资源的有序申请和释放、请求排队等。
相信大家在码字的时候,都遇到过电脑卡顿时,疯狂敲键盘没反应的情况,等它恢复后,又突然输入了很多字符。这就是因为计算机在用队列结构来处理你的按键请求,电脑CPU忙时没空处理按键请求,因此这些请求会积压在队列中,等CPU空闲了,会从队列中取出请求并执行,就出现了上述的情况。
2.1 排队锁
可以使用队列来做排队锁,JDK中的AQS就是用队列来做同步控制的。结合队列「先进先出」的特点,可以保证先申请锁的线程先执行,后拿到锁的线程后执行。
【分析】
初始化一个空队列,属性lock来标记当前是否上锁,属性owner标记当前锁的持有线程。当有线程来竞争锁时,CAS修改lock标记成功,则代表抢到锁,竞争失败的线程入队并阻塞,等待持有锁的线程释放锁并将它唤醒。
【实现】
public class QueueLock {
private Queue<Thread> queue;// 阻塞队列
private AtomicBoolean lock = new AtomicBoolean(false);//上锁标记
private volatile Thread owner;//锁的持有线程,避免误解锁
public QueueLock() {
queue = new LinkedBlockingQueue<>();
}
public void lock() {
Thread thread = Thread.currentThread();
while (!lock.compareAndSet(false, true)) {
queue.add(thread);
LockSupport.park();
}
owner = thread;
}
public void unlock() {
if (owner != Thread.currentThread()) {
throw new RuntimeException("无权释放锁");
}
owner = null;
lock.compareAndSet(true, false);
Thread thread = queue.poll();
if (thread != null) {
LockSupport.unpark(thread);
}
}
}
QueueLock这里实现的非常简单,目的只是为了说明队列的应用场景。
2.2 消息队列
队列的另一个大的应用场景就是消息队列,例如系统有一些比较耗时的操作,为了不阻塞主业务流程,可以发一个消息到消息队列,等消费者有时间了就去拉取消息并执行。
【分析】
初试化一个空的阻塞队列,支持向它注册多个消费者,当有生产者往队列中添加元素时,消费者去抢消息,抢到了就可以消费了。
【实现】
public class MessageQueue<T> {
private BlockingQueue<T> queue;
public MessageQueue() {
this.queue = new LinkedBlockingQueue<>();
}
// 注册消费者
public void register(Consumer<T> consumer) {
new Thread(() -> {
try {
T data;
while ((data = queue.take()) != null) {
consumer.accept(data);
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
// 生产消息
public void add(T data) {
queue.add(data);
}
public static void main(String[] args) throws Exception {
MessageQueue<Integer> queue = new MessageQueue<>();
queue.register(data -> {
System.out.println("消费者A:" + data);
});
queue.register(data -> {
System.out.println("消费者B:" + data);
});
Thread.sleep(1000);
for (int i = 0; i < 10000; i++) {
queue.add(i);
}
}
}
3. 总结
队列的特点是「先进先出」,只能在队头删除元素,在队尾添加元素。队列可以保证数据的顺序,先入队的数据具有更高的优先级,因此它非常适合用来做排队类的场景。