并发和多线程(二十)--LinkedBlockingQueue源码解析
阻塞队列在日常开发中直接使用比较少,但是在很多工具类或者框架中有很多应用,例如线程池,消息队列等。所以,深入了解阻塞队列也是很有必要的。所以这里来了解一下LinkedBlockingQueue的相关源码,从命名可以看出来是由链表实现的数据结构。
类定义
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
}
从上面可以看到继承Queue,实现BlockingQueue,我们介绍一下两个类里面方法的作用,为了方便记忆和对比放到表格里进行展示。
Queue
作用 | 方法1 | 方法2 | 区别 |
---|---|---|---|
新增 | add() | offer() | add()队列满的时候抛出异常,offer()队列满的时候返回false |
查看并删除 | remove() | poll() | remove()队列为空的时候抛出异常,poll()队列为空的时候返回null |
查看不删除 | element() | peek() | element()队列为空的时候抛出异常,peek()队列为空的时候返回null |
BlockingQueue
BlockingQueue顾名思义带有阻塞的队列,方法有所区别,下面方法包含了Queue,因为属于继承关系,下面表格方法名用序号代替。
作用 | 方法1 | 方法2 | 方法3 | 方法4 | 区别 |
---|---|---|---|---|---|
新增 | add() | offer() | put() | offer(E e, long timeout, TimeUnit unit) | 队列满的时候,1和2作用和queue相同,3会一直阻塞,4阻塞一段时间,返回false |
查看并删除 | remove() | poll() | take() | poll(long timeout, TimeUnit unit) | 队列为空,1和2没有变化,3会一直阻塞,4会阻塞一段时间,返回null |
查看不删除 | element() | peek() | 无 | 无 | 队列为空,1和2没有变化 |
成员变量
//链表的容量,默认Integer.MAX_VALUE
private final int capacity;
//当前存在元素数量
private final AtomicInteger count = new AtomicInteger();
//链表的head节点
transient Node<E> head;
//链表的tail节点
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();
//Node比较简单,一个item,还有指向下个节点,也就是单向链表
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; }
}
构造函数
//默认队列存储的元素最大为Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
//自定义capacity,初始化head、tail
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
putLock.lock();
try {
int n = 0;
//遍历集合,每次生成一个node,添加到链表尾部
for (E e : c) {
if (e == null)
throw new NullPointerException();
//每次判断新增的节点是否超过capacity,如果是,抛出异常
if (n == capacity)
throw new IllegalStateException("Queue full");
//将节点添加到队列tail
enqueue(new Node<E>(e));
++n;
}
//设置当前元素个数count
count.set(n);
//finally解锁
} finally {
putLock.unlock();
}
}
offer()
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
//初始设置为-1,c < 0,表示新增失败
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;
}
流程:
- 如果e为空,抛出异常。
- 如果当前队列已满,返回false。
- 将e封装成一个新的节点,加锁,putLock。
- 再次判断队列元素数量 < capacity,然后将node添加到链表tail。
- CAS将count+1,注意这里调用的是getAndIncrement返回的是+1之前的值。如果队列没满,唤醒某个某个因为添加而阻塞的线程。
- finally解锁,如果c == 0,加锁takeLock,唤醒继续添加。
- 返回 c >= 0。
put()
相对于offer(),put的代码会判断当前队列是否满了,如果满了,通过Condition阻塞,其他没啥区别。
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();
}
//注意这里c是先获取,后-1的
if (c == capacity)
signalNotFull();
return x;
}
流程:
- 加锁,takeLock。
- 如果当前队列为空,直接通过notEmpty阻塞,等待被唤醒。
- 取出第一个元素,并删除元素。
- 如果c > 1,表示队列还有元素,唤醒别的线程获取。
- finally解锁,如果c == capacity,表示队列没满,加锁takeLock,唤醒继续添加。
- 返回 x。
enqueue and dequeue
private void enqueue(Node<E> node) {
last = last.next = node;
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
这里统一讲一下在链表中添加和删除数据的流程,特别是dequeue(),我刚看第一眼的时候有点蒙蔽的,下面举个栗子。
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
Integer take = queue.take();
System.out.println(take);
这里把每一步都画出来了,还是比较好理解的。其余方法的逻辑都比较相似,下面简单说一下。
peek()
peek()和take()的代码差不多,只是不会删除元素,take()通过dequeue(),而peek()通过一句代码Node