队列是一个典型的FIFO先进先出结构,从一端放入元素,从另一端取出元素。队列在并发环境中比较有用,但是Java也提供了线程不安全的实现:LinkedList和PriorityQueue,ArrayDeque也实现了Queue的所有方法,它是基于可变数组实现的队列(关于ArrayDeque将在Deque章节中详细介绍)。
LinkedList的实现原理在前文中已经介绍过,下面给出LinkedList作为队列使用的一段示例代码,本文将对PriorityQueue作详细介绍。
LinkedList<String> linkedListDeque = new LinkedList<>(); linkedListDeque.offer("hello"); linkedListDeque.offer("i"); linkedListDeque.offer("am"); linkedListDeque.offer("queue"); org.junit.Assert.assertEquals("hello", linkedListDeque.peek()); org.junit.Assert.assertEquals("hello", linkedListDeque.poll());
Queue接口
我们先来看看Queue接口。
public interface Queue<E> extends Collection<E> { boolean add(E e); boolean offer(E e); E remove(); E poll(); E element(); E peek(); }
队列操作分为三种,每一种操作都提供了两个方法:一个会在操作失败时候抛出异常(比如容量限制等),一个会返回特殊的值,比如null或者boolean。
作用 | 抛出异常 | 返回特殊的值 |
---|---|---|
入队 | add(E e) | offer(E e) |
出队 | remove() | poll() |
获取元素 | element() | peek() |
PriorityQueue不允许元素是null,当poll操作出队时,返null是可行的。但是对于LinkedList的来说,它的元素允许为null,所以根据null来判断是否未空队列是不可行的。
PriorityQueue实现原理
PriorityQueue是一个优先级队列,FIFO队列也可以理解为优先级队列,出队的元素是等待时间最长的。PriorityQueue出队的元素则是优先级最高的元素,优先级是使用Comparator和Comparable,我们在《Java Collections Framework(一)概览》中已经描述过。
为了满足这样的结构,我们需要对这个优先级队列的元素进行排序,PriorityQueue是基于堆实现的一个无界队列,使用堆来实现优先级队列,排序的性能达到了O(lgN),如果使用数组来排序,一次排序的时间就可能是O(N)。
PriorityQueue内部维护了一个数组Object[] queue来实现平衡二叉堆的结构,即queue[n]的两个儿子是queue[2*n+1]和queue[2*(n+1)],所以它是有初始容量的,并且提供了容量增长策略。
初始容量
PriorityQueue的默认初始容量是由一个常量指定的,值为11,正如ArrayList一样,为了性能优化,它也提供了一个构造器传入初始容量。
private static final int DEFAULT_INITIAL_CAPACITY = 11; public PriorityQueue(int initialCapacity) { this(initialCapacity, null); }
接下来我们从入队来分析它的容量增长策略。
入队和增长容量策略分析
public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e); return true; }
可以看到,优先队列是不允许NULL值存在的,当元素个数超过内部数组长度时,就会扩容,我们接着看grow方法:
private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }
源码中可以看出容量增长策略:如果当前容量小于64,则扩容后容量是现有容量的两倍多2,否则容量增长现有容量的50%。
源码简要分析
peek
public E peek() { return (size == 0) ? null : (E) queue[0]; }
可见,是一个大根堆。
poll
public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; }
正如offer方法调用了siftUp方法,poll方法调用了siftDown方法来保证新增或者删除的时候,它仍然是一个堆。具体保证堆的算法这里就不贴出来了,可以自行查看源码或者算法书籍。
总结
并发包下面提供了很多有界线程安全的队列,提供了一些接口扩展了Queue,比如阻塞队列接口BlockingQueue,这些将在并发系列的文章中讲解。
- ArrayBlockingQueue 基于数组的有界阻塞队列,线程安全
- LinkedBlockingQueue 基于链表的可选有界阻塞队列,线程安全
- PriorityBlockingQueue 基于堆的无界阻塞优先队列,线程安全
- DelayQueue 基于堆的无界阻塞根据延迟时间优先顺序的队列,线程安全
- SynchronousQueue 同步队列,线程安全
- LinkedTransferQueue 线程安全
- LinkedBlockingDeque 基于链表的可选有界阻塞双向队列,线程安全