【数据结构】队列

19 阅读3分钟

队列(Queue)也是「线性表」的一种,它的特点是:先进先出,表的一端只能添加元素,另一端只能删除元素

添加元素叫作「入队」,删除元素叫作「出队」。入队一端叫作「队尾」,出队的一端叫作「队头」。

队列就和生活中的排队一样,得分个先来后到,大家不能插队,永远是先到的人先办事,后到的人后办事。
未命名文件 (19).jpg

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. 总结

队列的特点是「先进先出」,只能在队头删除元素,在队尾添加元素。队列可以保证数据的顺序,先入队的数据具有更高的优先级,因此它非常适合用来做排队类的场景。