首先看一下LinkedBlockingQueue继承关系
LinkedBlockingQueue简介
LinkedBlockingQueue是由链表实现的阻塞队列
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
因为每个节点会将元素封装成一个Node,从而实现链表结构,元素按照 FIFO 的形式来访问,队列头部为待的时间最久的元素,尾部则是最少,新元素插在尾部。
LinkedBlockingQueue源码分析
LinkedBlockingQueue主要成员变量
/** The capacity bound, or Integer.MAX_VALUE if none */
队列容量
private final int capacity;
/** Current number of elements */
元素数量
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
头节点,不存数据
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
尾节点,入队
private transient Node<E> last;
/** Lock held by take, poll, etc */
出队锁
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
出队等待条件
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
入队锁
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
入队等待条件
private final Condition notFull = putLock.newCondition();
通过几个主要成员分析,我们知道LinkedBlockingQueue是利用链表存储元素的,利用两个重入锁保证并发安全
LinkedBlockingQueue构造方法
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);
}
可以发现如果没有执行大小,那么队列的大小是Integer.MAX_VALUE的大小
初始化的同时初始化last和head,初始化一个空的节点
LinkedBlockingQueue添加元素
LinkedBlockingQueue–offer
add实际上调用子类的offer方法添加数据
public boolean offer(E e) {
// 判断是否为空
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 如果队列已经满了,返回false
if (count.get() == capacity)
return false;
int c = -1;
// 封装成Node节点
Node<E> node = new Node<E>(e);
// 获得putLock锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
// 判断元素的个数是否小于容量
if (count.get() < capacity) {
// 如果小于,则将node添加到链表中
enqueue(node);
// 获得元素数据,并进行增加
c = count.getAndIncrement();
// 如果发现增加后的数量小于容量
if (c + 1 < capacity)
// 唤醒notFull,通知入队可以继续添加数据
// 因为此时判断还没有达到队列最大容量
notFull.signal();
}
} finally {
putLock.unlock();
}
// 此时至少有一个元素放入到队列中了
if (c == 0)
// 唤醒
signalNotEmpty();
return c >= 0;
}
添加到链表中
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
// 从右往左赋值,将node的值给last节点的next
// 同时更新last节点为最新的node节点
last = last.next = node;
}
在判断c == 0的时候,说明此时至少有一个元素放入到队列中了,就可以唤醒取操作去取出元素
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
// 获得takeLock锁
takeLock.lock();
try {
// 通知取操作可以继续取元素
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
通过putLock的方式保证了操作队列尾部增加元素的原子性,同时如果插入成功会返回true,如果已经满了则丢弃当前元素返回false,所以这个方法是非阻塞的方法
LinkedBlockingQueue–put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
// 封装成node
Node<E> node = new Node<E>(e);
// 获取putLock锁
final ReentrantLock putLock = this.putLock;
// 获取元素个数
final AtomicInteger count = this.count;
// 加锁,此锁可以被中断,如果在加锁的过程中被其他线程设置了中断标志,则会抛出InterruptedException异常,从阻塞中唤醒
putLock.lockInterruptibly();
try {
// while循环判断元素个数是否等于队列大小
// 如果相等,则说明队列已经满了,则阻塞在notFull上
// 为什么要使用while循环,因为方式notFull虚假唤醒,导致已经满了的队列,依然往里面添加
while (count.get() == capacity) {
notFull.await();
}
// 添加元素
enqueue(node);
// 获取并增加元素数量
c = count.getAndIncrement();
// 判断是否小于队列数量
if (c + 1 < capacity)
// notFull唤醒,通知其他添加线程可以向队列中添加数据
notFull.signal();
} finally {
putLock.unlock();
}
// 通知其他出队线程可以取出数据
if (c == 0)
signalNotEmpty();
}
通过源码分析可以知道,如果队列有空间,则添加后直接返回,但是如果队列已经满了,则阻塞在notFull条件上,直到有其他线程唤醒,所以是阻塞的方法
LinkedBlockingQueue–poll
public E poll() {
final AtomicInteger count = this.count;
// 如果元素个数为0的话,直接返回null
if (count.get() == 0)
return null;
E x = null;
int c = -1;
// 获得takeLock锁
final ReentrantLock takeLock = this.takeLock;
// 加锁
takeLock.lock();
try {
// 如果数量大于0,说明队列中还有元素
if (count.get() > 0) {
// 取出元素
x = dequeue();
// 获取元素数量,并减少一
c = count.getAndDecrement();
// 如果发现移除完元素,还有元素,那么就通知其他取出线程,可以继续取出元素
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
// 判断之前的元素个数是否与队列大小相等,如果相等,说明之前队列是满的
// 移除一个元素后至少会有一个空位置,这时可以调用signalNotFull方法,通知添加线程可以继续添加元素
if (c == capacity)
signalNotFull();
return x;
}
通知添加线程可以继续添加元素
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
// 获取head节点
Node<E> h = head;
// 获取头节点的下一个元素,为新的头节点
Node<E> first = h.next;
// 将之前的头结点断开,将自己的引用赋给自己的next,此对象则不可达,垃圾回收会回收这个对象
h.next = h; // help GC
// 将新的first节点,也就是之前的next节点,赋给head节点
head = first;
// 返回之前next节点的item,也就是现在的first的item
E x = first.item;
// 将此时的item设置为null
first.item = null;
return x;
}
实际上head不负责保存数据,只是起到一个哨兵作用,实际返回的时候会返回head的next的item元素
而且poll操作,如果队列为空则返回null,这个方法是不阻塞的
LinkedBlockingQueue–take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
// 获取takeLock锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 判断元素数量是否为0,如果为0,说明为空,阻塞在notEmpty条件上
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;
}
与poll方法类似,获取队列中的头部元素,并从队列中移除,如果没有元素,则阻塞在notEmpty条件上,直到队列不为空,返回元素。
LinkedBlockingQueue–remove
public boolean remove(Object o) {
if (o == null) return false;
// 对整个队列进行加锁
fullyLock();
try {
// 从head开始遍历
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
// 如果发现p中的元素和需要移除的元素相等,说明找到了这个需要移除的元素
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
// 整个队列解锁
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}
void unlink(Node<E> p, Node<E> trail) {
// 将p的item设置为null
p.item = null;
// trail为p的上一个元素节点,将trail的next设置为p的next
// 所以就掠过了p这个元素
trail.next = p.next;
// 如果发现p是last节点,则重新设置last节点为trail
if (last == p)
last = trail;
// 获得元素个数并减一,如果和队列大小相等
// 此时可以唤醒添加线程继续添加元素,因为上面已经移除掉一个元素了
if (count.getAndDecrement() == capacity)
notFull.signal();
}
所以移除一个线程就是将上个一元素的next设置为这个元素的next
同样的remove方法是非阻塞的,如果队列中有则返回true,如果没有则返回false
LinkedBlockingQueue–总结
- LinkedBlockingQueue不允许添加null元素
- LinkedBlockingQueue是一个基于链表的队列,并且是一个FIFO队列
- LinkedBlockingQueue如果不指定队列的容量,默认容量大小为Integer.MAX_VALUE,有可能OOM
- LinkedBlockingQueue入队和出队操作采用了不同的锁来进行控制,这样入队和出队操作可以同时进行。但同时只能有一个线程可以进行入队或出队操作
- LinkedBlockingQueue内部采用的是可重入独占的非公平锁(ReentrantLock默认是非公平的锁),通过锁进行添加和取出的同步
- LinkedBlockingQueue通过原子变量count来记录元素个数(这里稍微说一下,因为添加和取出的时候都会对count进行修改,而且整个操作是原子性的,所以可以通过count返回队列中元素的个数,如果整个操作不是原子性的,则不能通过count返回队列中的元素个数)
LinkedBlockingQueue&ArrayBlockingQueue对比
- ArrayBlockingQueue底层是数组,容量有限制,初始化时需指定;LinkedBlockingQueue底层是链表,可以不指定大小,容量是int的最大值
- ArrayBlockingQueue维护一把锁,同一时刻只能有一个线程操作队列;LinkedBlockingQueue维护两把锁,同一时刻可以有两个线程在两段操作,互补干扰
- LinkedBlockingQueue中类似于remove的方法,需要对整个队列进行加锁
- LinkedBlockingQueue的吞吐量要高于 ArrayBlockingQueue
LinkedBlockingQueue在线程池中使用
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool使用的就是LinkedBlockingQueue队列,原因就是因为它的容量无限大(int的最大值),如果任务非常忙的时候,使用一个有界队列来处理,可能很快就满了,触发拒绝策略,而使用LinkedBlockingQueue队列,可以很好的适应任务繁忙的情况,即使任务非常多,也可以扩容,当处理完成之后,节点也会被清除掉,非常灵活
来源:CSDN
作者:我在青青草原抓羊
链接:https://blog.csdn.net/weixin_44936828/article/details/103833694