队列篇
前言
没想到这个系列居然这么快就又开始了。感觉面试官可能更加喜欢看过源码的同学,所以我们还是投其所好,在源码的地方多下点功夫吧。
今天这一篇是队列篇,算是之前留下来的一个坑,因为之前感觉Map和List问的多一些,所以就先总结了他们两个。
但是这次面试被问到了是否看过优先队列的源码,很惭愧的回答没有看过,所以还是把这一块的坑先填上,明天总结三大同步工具的源码。
PriorityQueue
接口与继承
先来看优先队列的源码,还是按照一贯的分析方式,先看继承和实现了什么接口。
可以看到优先队列继承了抽象类AbstractQueue
,这与ArrayList<>
类似。
AbstractQueue
同样和AbstractList
一样继承于AbstractCollection
,这个抽象类我们在之前的学习中已经见识过了。
//优先队列实现了序列化接口、继承自AbstractQueue
public class PriorityQueue<E>
extends AbstractQueue<E>
implements java.io.Serializable
//AbstractQueue继承自AbstractCollection实现了Queue接口
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E>
我们继续来看一下这个Queue
接口中有什么东西,可以看到只提供了一些常用方法比如add
添加元素之类的。
方法具体的区别我都已经写在上面了。
public interface Queue<E> extends Collection<E> {
//如果队列不允许空元素,传入null抛出异常
//如果指定元素的类阻止将其添加到此队列,则引发ClassCastException
//如果此元素的某些属性阻止将其添加到此队列,
//则引发IllegalArgumentException
//如果由于容量限制,此时无法添加元素,则引发IllegalStateException
boolean add(E e);
//同上,但是在容量不足的时候返回false而非抛出异常
boolean offer(E e);
//如果队列为空抛出异常
E remove();
//如果队列为空返回null
E poll();
//如果队列为空抛出异常
E element();
//如果队列为空返回null
E peek();
}
接下来看抽象类的实现,可以看到只实现了之前说到的一些会抛出异常的方法,比如add
,在add
方法中实际上也是调用offer
方法。
public abstract class AbstractQueue<E>
extends AbstractCollection<E>
implements Queue<E> {
protected AbstractQueue() {
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public void clear() {
while (poll() != null)
;
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
}
成员变量
优先队列的成员变量并不多,和ArrayList的成员变量类似,拥有modCount
参数用于快速失败,底层都是一个数组,并且使用size
来表示有效元素的多少。
这里有必要说一下数组最大长度,我之前一直以为是为了防止数组溢出而设置一个阈值,当达到这个值的时候下次扩容就把值变为整型最大值。
其实根据上面的注释说明是:有些虚拟机会在数组头部设置一些参数,所以最大只能申请Integer.MAX_VALUE - 8
大小。
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue; // non-private to simplify nested class access
private int size = 0;
private final Comparator<? super E> comparator;
transient int modCount = 0; // non-private to simplify nested class access
//数组最大的长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
插入和删除
对于优先队列我们重点还是放在插入过程这边,不过在这之前我们需要先复习一下什么是堆排序,这里有一篇文章写得很不错,我就不班门弄斧了。
深刻理解归并排序、快速排序与堆排序(附代码)
在理解了堆排序的原理之后,我们再来看源码,先来看插入的源码,如果有效元素个数已经大于等于了长度,这个时候就需要扩容,具体和ArrayList类似,不同的地方在于小于64扩为2倍,否者1.5倍,这里就不展开了。
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
//扩容,小于64扩为2倍,否者1.5倍
//具体和ArrayList类似,就不展开了
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
//向上筛选
siftUp(i, e);
return true;
}
主要来看向上筛选的过程。首先队列会根据是否存在比较器来使用默认的排序方式还是比较器的排序方式。
向上筛选的过程比较简单,因为原来就是一个有序的堆,所以这里只要找到其父亲节点,然后比较是否有序。
如果有序则直接退出。
如果是逆序的话就需要交换两个元素,并且继续向上比较。
我终于明白了为什么小顶堆的lambda表达式是(v1,v2)->(v1-v2)
,原来v2是插入的元素,v1是父亲节点,我一直以为v1是插入的元素。
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
接下来看删除的过程。这里调用了siftDown
方法,删除数组中的第一个元素之后,把最后一个元素移动到队列头部来,然后从上向下再进行一次调整。
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;
}
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
//从上到下的调整只要调整到最后一个非叶子节点
//就说明这条路径已经有序了
int half = size >>> 1;//找到第一个非叶子节点
while (k < half) {
int child = (k << 1) + 1;//找到其左子节点,该节点必定存在
Object c = queue[child];
int right = child + 1;
//判断右子节点是否存在,如果存在则使用右子节点进行交换
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
//已经有序
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
堆化和随机删除
堆化的过程其实就是堆排序的过程,可以看到这也是一个从上向下调整的过程,需要把在第一个非叶子节点之前的节点全部调整一遍。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
随机删除过程比较复杂,首先和之前的删除操作一样,如果元素不是末尾元素,移除元素之后会把末尾元素放到移除元素的位置上。
然后从移除位置开始先向下调整,如果该元素调整之后还在原来的位置上就向上调整。
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
ArrayBlockingQueue
看完了优先队列,我们继续来看有界阻塞队列,这一部分感觉问的不是很多,而且一共好像有8种阻塞队列所以就挑两个种常用粗看一下吧。
继承和接口
ArrayBlockingQueue
同样继承于抽象类AbstractQueue<E>
但是它实现的是BlockingQueue<E>
接口而非Queue<E>
接口,那我们来看一下这两个接口有什么不同。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
BlockingQueue<E>
继承于Queue<E>
,在后者的基础上增加了一些方法。
首先是put
方法和take
方法。
put
方法和add
的区别主要在于如果队列已满,那么阻塞直到队列能够加入这个元素,而add
会直接抛出异常。
take
方法也是类似,会阻塞等待,直到队列中有元素。
出此之外还增加了两个有时间限制的offer
和poll
方法。
最后的两个方法是用于将当前队列的元素移动(会删除队列中的元素)给传入的集合。
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);
boolean offer(E e);
//阻塞插入
void put(E e) throws InterruptedException;
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
//阻塞获取
E take() throws InterruptedException;
E poll(long timeout, TimeUnit unit)
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
//复制元素
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
成员变量
主要的成员变量如下,从注释风格可以看出显然不是写优先队列的人写的,和并发哈希表更接近,这是不是说明了并发是同一个人写的?
优先队列由于堆化,所以数组的最前面一定是有元素,并且是连续的,所以不需要使用指针指向插入和删除的位置。
但是有界阻塞队列需要两个指针指向操作的位置,但是为什么不是使用volatile
关键字标记的呢?
因为ArrayBlockingQueue
非常粗暴,插入和删除都是采用可重入锁直接锁住整个数组的,所以不需要使用volatile
来维护可见性。
notEmpty
和notFull
则是两个条件,用于唤醒等待的线程。关于条件唤醒和Callable
之后会再写一篇文章进行分析。
最后的itrs
是一个线程安全迭代器。
final Object[] items;
//删除位置
int takeIndex;
//插入位置
int putIndex;
//元素个数
int count;
//可重入锁
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
//线程安全迭代器
transient Itrs itrs = null;
构造方法
ArrayBlockingQueue
一共有三个构造方法,可以看到队列长度是必须要传的。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
插入和删除
插入操作首先会判断数组的有效元素个数,然后会进行不同的操作,如果能够插入,则会调用enqueue
方法,相对的,如果是删除操作则会调用dequeue
方法。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取锁之后可以被其他线程中断
//中断之后会直接抛出异常
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
那么入队和出队操作又是怎么执行的呢?
代码很短,如果插入的位置已经超过了数组长度,那么就回到数组开头。
然后在插入完成之后唤醒等待的take
和put
线程。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
//注意在进入之前已经获得了锁
//由于队列满的时候是不会插入元素,所以可以断言插入的位置为null
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
//调整一下迭代器的位置
itrs.elementDequeued();
notFull.signal();
return x;
}
LinkedBlockingQueue
还是先来看继承和接口,这些接口和抽象类我们之前都看过了,所以直接来看成员变量吧。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable
成员变量
成员变量和普通的LinkedList<>
类似,都有头尾节点,采用了两个不同的锁来提高效率。
private final int capacity;
//原子整型,因为链表插入和删除操作大部分时间不会相互影响
//所以需要使用原子整型进行更新
private final AtomicInteger count = new AtomicInteger();
//头结点和尾结点,两个都是虚拟节点
transient Node<E> head;
private transient Node<E> last;
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
构造方法
说明LinkedBlockingQueue
可以是一个无界队列。
如果在线程池中采用可能会导致OOM。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
插入和删除
enqueue
是节点的插入操作,put
操作不同的在于是对入队锁加锁,因为对于节点的插入只对尾结点的next指针进行操作,而删除只对head的next指针进行操作,所以不会发生同步问题。
插入成功之后会更新元素个数,count是一个原子整型。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
后记
夜深了,人困了,就暂时写到这里吧。
明天填一下Callable
和同步工具的坑,话说好久没写SQL了,如果到时候面试写不出不是很尴尬,所以明天开始捡起来再练习一下吧。