LinkedBlockingQueue
是一个阻塞队列,采用双锁技术,性能较 ArrayBlockingQueue
有很大的提高。
源码层面,有很多地方与ArrayBlockingQueue原理类似。前面写过一篇ArrayBlockingQueue源码解析, 看这篇文章之前,可以先看下。
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Thread t0 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 10; i++) {
boolean offer = queue.offer(i);
log.info("offer:{}, value:{}", offer, i);
Thread.sleep(300);
}
}
}, "t0");
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Integer take = queue.take();
log.info("take:{}", take);
}
}
}, "t1");
t1.start();
t0.start();
}
上面是一个demo,一个线程往队列中放,一个线程从队列中取。当队列为空时,放的操作会阻塞。
下面以这个为例,详细说明其源码。
一、初始化
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
这行代码,可以传一个数字进去,指队列的长度。如果不传,默认是 Integer 的最大值
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity; // 队列容量
last = head = new Node<E>(null); // 初始化节点
}
另外,有必要了解下LinkedBlockingQueue的几个固有属性
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(); // 入队等待
二、入队源码解析
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(); // 队列中元素计数加1,并返回原来的个数
if (c + 1 < capacity)
notFull.signal(); // 队满有空位,唤醒其它放元素的线程
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty(); // 唤醒出队线程
return c >= 0;
}
enqueue(node)
,这段代码很简单,底层是链表,追加一个就可以了。
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
// Node 是一个内部类,item记录入队的元素, next 指向下一个节点
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
signalNotEmpty()
这个方法,本质是 AQS里的 signal方法,用一句话概括
就是从等待队列中取一个节点,放入CLH队列(抢锁队列)
详细的分析可以我的另一篇博客中查找:signal源码分析。
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
从源码上看,是 c == 0 时调用 signalNotEmpty
这个方法,为什么?
正常情况下,c = count.getAndIncrement(),看仔细了,是入队前队列中元素的个数。
c == 0, 即原来队列是空的,现在放了一个进去。
原来空队列时,有可能会有取元素的线程在等待,所以,要唤醒下。
return c >= 0
这个开始我没想明白,什么情况下,c 会等于 -1
if (count.get() < capacity) 当队满时,这个条件为false,直接就释放锁,c == -1,
即往队列中放元素失败
三、出队源码
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;
}
这里的 notEmpty.await()
用一句话概括:释放锁,阻塞。当线程被唤醒时,抢锁,直到抢到为止。
notEmpty.signal()
用一句话概括:从等待队列中取出一个节点,放到CLH队列(抢锁队列)
这两个方法,源码解析,可以看我的另外一篇文章:awtit() 与 signal()
出队的最后一段代码,是当原队列是满的时候,此次出队,触发唤醒入队的线程。
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
四、阻塞与唤醒
前面讲入队方法 offer(e),这个方法是无阻塞的,返回 true,入队成功,返回 false,入队失败。
入队方法 put(e),这方法是阻塞的,即队列满时,线程阻塞,直到有队列中有空位时,执行入队。
下面我用几张图来说明,入队与出队时,阻塞与唤醒的具体场景(put–入队, take–出队)
队列的长度设定为3,先有五个线程(t1 - t5)往队列里放元素。队列长度是3,有两个线程会阻塞
这时,假设 t6线程来取元素,这是队满时取元素,最后会执行signalNotFull()
这个方法
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
notFull.signal() 这行代码执行后,就是这么一个场景
putLock.unlock()
这行代码执行时,会唤醒t4线程(这篇文章中详细说明 解锁源码)
t4 是往队列中放的时候,没位置阻塞了。t6取出一个元素后,就有了一个空位,t4被唤醒了,就继续执行,往队列中放。
在 t4 往队列中放的时候,如果还有空位,那还会去唤醒 t5 ,执行放元素的操作。
putLock 与 takeLock 之间在队满或队空的时候,相互之间会进行通讯。
总结
LinkedBlockingQueue 采用双锁,即入队有入队的锁,出队有出队的锁。
入队方法,put()
队满时阻塞, offer()
队满时,直接返回false
出队方法,take()
队空时阻塞,poll()
队空时,返回null