上一篇我们学习了ArrayBlockingQueue的实现原理,这一篇我们来学习与之对应的LinkedBlockingQueue。很明显,ArrayBlockingQueue内部是基于数组实现的,而LinkedBlockingQueue是基于链表。他们都实现了阻塞队列的take和put方法,下面我们会结合ArrayBlockingQueue作对比,来分析LinkedBlockingQueue的实现原理。
1 LinkedBlockingQueue简介
Integer.MAX_VALUE
。除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。
2 LinkedBlockingQueue类图结构
3 构造方法
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);
}
4 添加元素
offer方法
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity) //队列已满,则返回false
return false;
int c = -1;
Node<E> node = new Node<E>(e); //构造节点
final ReentrantLock putLock = this.putLock; //取得写入锁
putLock.lock();
try {
if (count.get() < capacity) { //需要在写入之前再次判断
enqueue(node); //入队
c = count.getAndIncrement(); //原子操作,防止读线程竞争冲突
if (c + 1 < capacity)
notFull.signal(); //如果未满,通知写入线程
}
} finally {
putLock.unlock(); //解锁
}
if (c == 0) //如果写入成功
signalNotEmpty(); //通知读取线程
return c >= 0;
}
这里讨论下,为什么插入操作后也要通知写入线程:因为A、B线程同时写,A获得锁,B被阻塞,因此,A完成后需要立刻通知B线程写入,而不是等到读取线程给B通知。
put方法
将指定元素插入到此队列的尾部,如有必要,则等待空间变得可用。源码如下:
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(); //如果中断,则抛出异常并退出。避免了取得锁后调用wait时才发现中断
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) { //每次唤醒则获取锁 检查容量 容量已满则继续阻塞
notFull.await();
}
enqueue(node); //容量未满 插入操作
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); //未满 通知插入线程
} finally {
putLock.unlock(); //解锁
}
if (c == 0)
signalNotEmpty(); //通知读取线程
}
5 获取元素
peek方法
获取但不移除头元素,若为空返回null。源码如下:
public E peek() {
if (count.get() == 0) //元素为空返回null
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock(); //获取锁
try {
Node<E> first = head.next; //头结点的next即为第一个结点
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
peek方法从头节点直接就可以获取到第一个添加的元素,所以效率是比较高的。如果不存在则返回null。
poll方法
poll方法获取并移除此队列的头,如果此队列为空,则返回 null。源码如下:
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock; //获取锁
takeLock.lock(); //加锁
try {
if (count.get() > 0) { //如果有元素 则取出
x = dequeue();
c = count.getAndDecrement(); //更新count
if (c > 1)
notEmpty.signal(); //通知其他读取线程
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull(); //通知写线程
return x;
}
take方法
获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。源码如下:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock; //获取锁
takeLock.lockInterruptibly(); //加锁
try {
while (count.get() == 0) { //若已经空,则阻塞
notEmpty.await();
}
x = dequeue(); //出队
c = count.getAndDecrement();
if (c > 1) //若队列中还有元素 则唤醒其他读取线程
notEmpty.signal();
} finally {
takeLock.unlock(); //解锁
}
if (c == capacity) //若不满 唤醒插入线程
signalNotFull();
return x;
}
6 总结
ArrayBlockingQueue和LinkedBlockingQueue的不同
1)内部实现不同
ArrayBlockingQueue使用数组,而LinkedBlockingQueue使用链表。
2)锁的实现不同
ArrayBlockingQueue入队出队都使用同一把锁,而LinkedBlockingQueue使用了两把锁。因此,ArrayBlockingQueue的入队出队操作是同步的,而LinkedBlockingQueue是可以并行的。这里的根本原因是:插入操作时,LinkedBlockingQueue的head不会影响head节点,而出队操作也不会影响tail节点。所以可以并行。
3)初始化条件不同
ArrayBlockingQueue需要确定队列大小而LinkedBlockingQueue不需要,具有默认值Integer.Max_VALUE。