相比ArrayBlockingQueue,LinkedBlockingQueue使用链表结构实现阻塞队列,使用的辅助类更多一些,一般说是无界队列,实际上也是有界的,只不过上限是int类型的最大值而已,也就是40多亿,一般达不到这个数量,在达到之前估计计算机内存就爆了。只不过ArrayBlockingQueue初始化的时候已经初始化好了数组,因此内存也分配好了,而链表是动态增减的,内存使用率可能更好一些。
成员变量
//容量队列
private final int capacity;
/** 队列长度 */
private final AtomicInteger count = new AtomicInteger(0);
/**
*队列头
*/
private 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();
存放元素的为一个内部类
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
初始化
构造函数共3个
// 队列默认最大长度为2^32^
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();
}
}
插入元素
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);
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();
}
元素入队时调用了enqueue方法
private void enqueue(Node<E> node) {
//将将新节点设为新队尾
last = last.next = node;
}
发出不为空队列信号时
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
offer(E)
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);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
//此时尚未满队 等达到capacity时就不再通知入队了
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
节点数量为0,说明队列原先是空的 唤醒一个队列读取元素
if (c == 0)
signalNotEmpty();
return c >= 0;
}
获取元素
take 阻塞式出队
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//(1)
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//收到队列非空信号后 并且重新获取到锁后要再一次检查队列容量 有可能其他收到的信号线程抢先一步拿走元素 重新变成空队列
while (count.get() == 0) {
//(2)
notEmpty.await();
}
//从队头获取一个元素
x = dequeue();
//容量减一
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
//(3)
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
//获取队头
Node<E> h = head;
//获取队头后的节点
Node<E> first = h.next;
//队头节点指针指向本身帮助GC
h.next = h;
//队头设置为下一个节点
head = first;
//返回这个节点的值 并将其item设置为null 作为一个空队头
E x = first.item;
first.item = null;
return x;
}
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();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
//原先是满队列 出队一个后肯定不满了
if (c == capacity)
signalNotFull();
return x;
}
peek 非阻塞式获取队首元素 与take/poll的区别是只返回队首 而没有让其出队
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();
}
}
remove 移除元素 内部要同时获取到同时对入队锁与出队锁 后才能移除元素
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();
}
}
删除节点是调用的是unlink
/*
* p 要移除的节点
* trail p节点的前置节点
/
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
//移除的节点是队尾节点时 更新一下队尾引用变量
if (last == p)
last = trail;
//原队列是满队列时 发出不满信号
if (count.getAndDecrement() == capacity)
notFull.signal();
}
ArrayBlockingQueue与LinkedBlockingQueue在入队时采用的队列是否为空的信号发送逻辑有明显的不同,相比之下后者似乎更高明一些
相同点:
- 两者均采用两个等待队列 notEmpty与notFull
- 都是在入队时发出notEmpty信号,出队时发出notFull信号
- 非阻塞式方法offer没有获取到锁或者容量达到上限时,不会发出notFull信号
- 非阻塞式方法poll没有获取到锁或者容量达到上限时,不会发出notEmpty信号
不同点:
- ArrayBlockingQueue 入队 时每次都会发出notEmpty信号,
- ArrayBlockingQueue 出队 时每次都会发出notFull信号,
- LinkedBlockingQueue入队时可能会同时发出notEmpty与notEmpty信号,
- LinkedBlockingQueue入队时但只在当前容量小于(最大容量-1)时才会发出,因为等于时队列已经满了,再发信号也没用,已经入不了队了
- LinkedBlockingQueue入队时发出notEmpty信号的代码块不在插入元素时的锁控制范围内,而是重新拿全局锁锁控制的,并且有一个判断条件,只在插入节点前队列长度为0时才会发出信号
- LinkedBlockingQueue出队时只会在出队前队列长度大于1时发出notEmpty信号
- LinkedBlockingQueue出队时只会在出队前队列长度等于队列最大长度时发出notFull信号
根据异同点可以得知在队列经常达到满格或者变成空队列的场景下,LinkedBlockingQueue发送信号的次数更少一些。
判断信号发送时机的逻辑十分精巧,我们来仔细分析一下
发出信号的目的
出队函数take方法中发现队列是空的时候需要等待元素入队,入队函数put方法中发现队列已经满了的时候需要等待队列不满,此时两者调用Condition的await方法来阻塞线程,需要其他线程调用对应Condition的signal/signalAll函数来激活线程
信号激活的线程
- notEmpty激活的是出队锁takeLock持有的线程
- notFull激活的是入队锁putLock持有的线程
为何发送信号时采用的是Condition的signal而不是signalAll函数
因为入队与出队采用的是独占锁,因此等待队列中只会存在一个线程,故采用消耗更少的signal而没有用signalAll函数
发送notEmpty信号时机分析
发送点:
- 入队线程释放入队锁后 发现入队前是空队列
- 出队线程释放出队锁前 发现出队后队列还不为空
为什么只在插入节点前是空队列的情况下发出notEmpty信号
队列变化的过程中,发生出队等待的唯一条件是队列变成空队列,此时调用await方法阻塞,释放出队锁,此时可能有多个出队线程进入等待队列。
由于入队锁只有一个且是独占锁,所以入队相当于一个串行程序,因此第一次入队与后面每次出队造成的空队列均能被入队线程检测到,此时入队完成后发一个队列不为空信号激活一个出队线程。
由于出队锁也是独占锁,也相当于一个串行程序,释放锁前会检查一下队列是否为空,不为空则发出不为空信号,激活下一个线程,于是等待中的出队线程开始“接力复活”,队列继续运行,一切ok。
发送notFull信号时机分析
发送点:
- 出队线程释放出队锁后 发现出队前队列是满的
- 入队线程释放入队锁前 发现队列还不满
入队线程拿到入队锁后发现队列已经满了,于是释放锁开始等待,因此最终可能有多个入队线程在此处等待。入队线程激活的唯一条件是收到一个队列不满的信号,这个信号只可能由出队线程发出。
入队锁与出队锁都是有一个且都是独占锁,因此各自都相当于一个串行程序,每次出队线程释放出队锁后都会检查出队前的队列长度,因此一定会有一个线程发现队列之前是满的,于是发出不满的信号激活任意一个入队线程,而入队线程完成入队任务后看到队列还没满又会发出notFull信号来激活下一个入队线程,由此展开接力赛,一切都在滚滚向前。
由上可以看出出队线程与入队线程,一方陷入等待后变由对方激活任意一个,激活的那一个线程在完成任务后会再次检测队列长度,满足条件会再随机激活己方的一个线程进行接力赛,这种接力赛式的逻辑实在是巧妙的设计,令人叹服!!!