LinkedBlockingQueue,基于链表实现的先进先出队列,与ArrayBlockingQueue相比,LinkedBlockingQueue的重入锁被分成两部分,分别对应存值和取值(被称作双锁队列算法),因此可以同时进行读操作和写操作,所以理论上吞吐量超过ArrayBlockingQueue。
主要成员变量
/**
* 链表节点类
*/
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
/** 最大容量 */
private final int capacity;
/** 当前队列长度,采用AtomicInteger原子增减 */
private final AtomicInteger count = new AtomicInteger();
/** 头结点 */
transient Node<E> head;
/** 尾结点 */
private transient Node<E> last;
/** 取值锁(take/poll等) */
private final ReentrantLock takeLock = new ReentrantLock();
/** 表示“队列非空”的对象监视器 */
private final Condition notEmpty = takeLock.newCondition();
/** 存值锁(put/offer等) */
private final ReentrantLock putLock = new ReentrantLock();
/** 表示“队列非满”的对象监视器 */
private final Condition notFull = putLock.newCondition();
主要方法
①signalNotEmpty()和signalNotFull()
signalNotEmpty() 负责唤醒对应的出队操作
signalNotFull() 负责唤醒对应的入队操作
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
思考:为什么要封装signal方法?
以put方法为例(具体源码看方法②),put方法是队列存值操作,需要使用putLock进行加锁,但是存值方法执行完后,队列内有值存在,需要takeLock锁对notEmpty进行唤醒,为了方便调用对signal方法进行封装。
②put(E e)
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
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);
// count原子操作自增1,相当于count++
c = count.getAndIncrement();
// 如果c+1<容量,表示可以通知非满状态对象监视器,阻塞的入队操作可以进行入队
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// c == 0说明count是从0变化到1,表示队列非空,需要通知等待队列非空状态的线程(例如阻塞的出队操作等)
if (c == 0)
signalNotEmpty();
}
插曲!!!
上面的源码中最后两行if判断c==0的地方,一开始看的时候发现有问题,问题是count代表队列的元素数量,当队列为空时,count=0,进行自增后赋值给c,此时c=1,必不可能存在c=0的情况,仔细研究发现问题的关键在于自增方法,源码中用的是c = count.getAndIncrement();是先获取count赋值给c后,count++,还有一种是c = count.incrementAndGet();是先count++后赋值给c,这样问题就解决了,当队列为空时count = 0,进行put入队操作,首先把count = 0赋值给c然后count++,此时c = 0,队列由原来的空变为有一个元素,此时唤醒等待队列非空状态的线程。
代码示例:
public static void main(String[] args) {
int c1 = -1;
AtomicInteger count1 = new AtomicInteger(0);
c1 = count1.getAndIncrement();
System.out.println(c1);
int c2 = -1;
AtomicInteger count2 = new AtomicInteger(0);
c2 = count2.incrementAndGet();
System.out.println(c2);
}
③offer(E e)
尝试入队一次,如果失败则返回false,offer和put方法的区别就是offer判断count是否达到了最大容量,即判断队列是否已满,如果队列已满则直接返回false而put方法会阻塞入队操作。
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
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;
}
④offer(E, long, TimeUnit)
此入队方法会阻塞入队操作指定时间,并在这个时间段内不断尝试入队操作,超时后返回false。此方法跟put方法类似,但是put方法会一直阻塞线程并尝试入队,直到成功。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
// 将timeout转换成纳秒数
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
⑤remove(Object) 删除指定对象
public boolean remove(Object o) {
if (o == null) return false;
fullyLock();
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
fullyLock()和fullyUnlock()会对putLock()和takeLock()统一上锁或者解锁,这是因为LinkedBlockingQueue是一个双向链表,remove可能会同时影响入队和出队操作
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
⑥take() 不停尝试出队,与put()方法类似,会一直尝试出队操作
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();
// 如果c>1表示队列非空,当c=1时,此时count=1后进行原子自减,队列为空
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 当c等于最大容量时,count等于最大容量然后自减,此时队列非满,唤醒等待队列非满的线程
if (c == capacity)
signalNotFull();
return x;
}
⑦poll() 和 poll(long, TimeUnit)
这两个方法与offer()和offer(E, long, TimeUnit)类似,poll()只会尝试一次出队,失败返回null,poll(long, TimeUnit)会不停的尝试出队,超时后返回null,这两个方法的源码逻辑跟入队差不多,只贴出来不做解析了。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
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();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
⑧peek() 查看队头元素,注意是查看,不是取出,源码很简单,没什么可注释的
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
ArrayBlockingQueue有界阻塞队列和LinkedBlockingQueue无界阻塞队列对比:
相同点:
1、LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列,内部都是使用锁ReentrantLock和对象监视器Condition来保证生产和消费的同步。
2、当队列为空,出队线程即消费者被阻塞;当队列已满,入队线程即生产者被阻塞。
不同点:
1、锁机制不同
LinkedBlockingQueue中的锁是分离的,入队锁(生产者)putLock,出队锁(消费者)takeLock,而ArrayBlockingQueue出队和入队都使用同一把锁。
2、底层实现不同
LinkedBlockingQueue内部维护的是一个链表结构。在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。而ArrayBlockingQueue内部维护了一个数组在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
3、构造初始容量不同
LinkedBlockingQueue有默认的容量大小:Integer.MAX_VALUE,当然也可以传入指定的容量大小,ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值
4、队列元素个数count定义不同
LinkedBlockingQueue中定义的count是AtomicInteger类型,ArrayBlockingQueue中定义的count是int类型。