LinkedBlockingQueue简介:
LinkedBlockingQueue 中文叫做链表阻塞队列,单从名字就可以得知其底层的实现是由链表来实现的。其原理为使用链表+锁+迭代器来完成。有链表实现的阻塞队列,队列的尾为链表的尾节点,队首为链表的头节点。在入对和出对都由ReentrantLock实现的takeLock和putLock来实现加锁从而到达,线性安区和队列阻塞。
其关心类图如下:
从类图中可知LinkedBlockingQueue->BlockingQueue -> Queue -> Collection,BlockingQueue 和 Queue 都是定义了一些queue基本操作,具体实现由子类实现,同时实现了。
其成员变量
// 链表存放queue数据
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
/** 如果传过来的队列的大小为空则默认是nteger.MAX_VALUE 最大值 */
private final int capacity;
/** 队列的包含的个数*/
private final AtomicInteger count = new AtomicInteger();
/**
* head.item == null
* 链表头
*/
transient Node<E> head;
/**
* 链表尾节点
* Invariant: last.next == null
*/
private transient Node<E> last;
/**
* 锁 begin
* take时锁
*/
private final ReentrantLock takeLock = new ReentrantLock();
/** 基于AQS同步机制的条件队列*/
private final Condition notEmpty = takeLock.newCondition();
/** put时锁,takeLock和putLock两把锁,主要是为了put和takeLock可以同时进行 */
private final ReentrantLock putLock = new ReentrantLock();
/** put的条件队列 */
private final Condition notFull = putLock.newCondition();
// 锁结束
常用的 方法:
- 入对操作:
add 队列满的时候抛出异常;
offer 队列满的时候返回 false。 - 查看并出对操作:
remove 队列空的时候抛异常;
poll 队列空的时候返回 null。 - 只查看不删除操作:
element 队列空的时候抛异常;
peek 队列空的时候返回 null。
这六个方法的区别:
遇到队列满或空的时候,抛异常,如 add、remove、element;
遇到队列满或空的时候,返回特殊值,如 offer、poll、peek。
其构造器:
- 无参构造器
- 传入一个capacity容量的构造器
- 传入一个集合的构造器。
源码如下:
/**
* 一个无参构造方法,此时queue的大小为Integer最大值
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 有参构造器,此时队列的大小为传过来的capacity的大小
*
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
/**
* 传入一个Collection对象,此时queue的大小为Integer最大值
*/
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为例:
put 方法在碰到队列满的时候,会一直阻塞下去,直到队列不满时,并且自己被唤醒时,才会继续去执行,
/ 把e新增到队列的尾部。
// 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
public void (E e) throws InterruptedException {
// e 为空,抛出异常
if (e == null) throw new NullPointerException();
// 预先设置 c 为 -1,约定负数为新增失败
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 设置可中断锁
putLock.lockInterruptibly();
try {
// 队列满了
// 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
while (count.get() == capacity) {
// await 无限等待
notFull.await();
}
// 队列没有满,直接新增到队列的尾部
enqueue(node);
// 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
// 这里的 c 是比真实的 count 小 1 的
c = count.getAndIncrement();
// 如果链表现在的大小 小于链表的容量,说明队列未满
// 可以尝试唤醒一个 put 的等待线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
// c==0,代表队列里面有一个元素
// 会尝试唤醒一个take的等待线程
if (c == 0)
signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
last = last.next = node;
}
出对分析:(原理大致同入对)
整体流程和 put 很相似,都是先上锁,然后从队列的头部拿出数据,如果队列为空,会一直阻塞到队列有值为止。
// 阻塞拿数据
public E take() throws InterruptedException {
E x;
// 默认负数,代表失败
int c = -1;
// count 代表当前链表数据的真实大小
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 空队列时,阻塞,等待其他线程唤醒
while (count.get() == 0) {
notEmpty.await();
}
// 非空队列,从队列的头部拿一个出来
x = dequeue();
// 减一计算,注意 getAndDecrement 返回的值是旧值
// c 比真实的 count 大1
c = count.getAndDecrement();
// 如果队列里面有值,从 take 的等待线程里面唤醒一个。
// 意思是队列里面有值啦,唤醒之前被阻塞的线程
if (c > 1)
notEmpty.signal();
} finally {
// 释放锁
takeLock.unlock();
}
// 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
if (c == capacity)
signalNotFull();
return x;
}
// 队头中取数据
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;// 头节点指向 null,删除
return x;
}
查看peek
接把队列头的数据拿出来即可
// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
// count 代表队列实际大小,队列为空,直接返回 null
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();
}
}