LinkedBlockingQueue
基于链接节点的可选有界阻塞队列。此队列对元素进行 FIFO(先进先出)排序。队列的头部是在队列中时间最长的元素。队列的尾部是在队列中时间最短的元素。新元素被插入到队列的尾部,队列检索操作获取队列头部的元素。链接队列通常比基于数组的队列具有更高的吞吐量,但在大多数并发应用程序中性能更不可预测。
可选的容量绑定构造函数参数用作防止过度队列扩展的一种方式。容量,如果未指定,则等于Integer.MAX_VALUE
。链接节点在每次插入时动态创建,除非这会使队列超出容量。
用法
LinkedBlockingQueue可以是有界队列,也可以是无界队列。数组的扩容是一件很麻烦的事情,因此ArrayBlockingQueue必须指定容量,数组一经创建就不可变了,它只能做有界队列。而LinkedBlockingQueue由于使用的是链表结构,元素的增删很容易实现,不需要初始化时就开辟内存,更适合做无界队列。默认的构造函数中,LinkedBlockingQueue的容量为Integer.MAX_VALUE,可看作是无界队列。
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
String s = mQueue.take();
System.out.println("取出数据:" + String.valueOf(s));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (true) {
System.out.println("装载数据:" + count);
try {
mQueue.put(String.valueOf(count));
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}).start();
}
}
成员变量
// 容量,默认为Integer.MAX_VALUE
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();
ArrayBlockingQueue使用int变量记录数量,而LinkedBlockingQueue使用AtomicInteger原子类记录数量。这是因为ArrayBlockingQueue生产消费使用同一把锁,任一时刻最多只有一个线程可以修改count。而LinkedBlockingQueue生产和消费使用的是不同的锁,两者线程都可以修改count,存在线程安全问题,因此使用原子类来保证数据安全。
head和last分别指向链表的首尾节点,很好理解。takeLock和putLock分别是消费者和生产者竞争的锁,每把锁各自有一个等待队列Condition,当队列满时,线程会被放入notFull等待,队列空时,线程会被放入notEmpty等待。
构造函数
默认容量为Integer.MAX_VALUE
,可看作是一个无界队列。使用无界队列是比较危险的,当生产速度远高于消费速度时,会导致大量数据堆积,JVM内存溢出。
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);// 头尾节点指向一个空的Node节点
}
默认情况下,头尾指针会指向一个空的Node节点,因此,链表中至少会有一个节点。
生产元素
put在此队列的尾部插入指定元素,如有必要,等待空间可用。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(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)//0代表队列有一个元素了。
signalNotEmpty();//有一个元素时开始通知消费线程消费数据
}
private void enqueue(Node<E> node) {
// 尾节点指向新node,前任尾节点的next指向新node
last = last.next = node;
}
它和offer
方法的区别就是,当队列已满,它不会直接返回false,而是调用notFull.await
无限期的阻塞,直到被唤醒。
消费元素
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();
}
if (c == capacity)// 队列中还有 capacity-1 个元素
signalNotFull();// 通知生产者线程,只通知一次,生产者会自行通知
return x;
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;// head指向head.next
E x = first.item;
first.item = null;
return x;
}
它和poll
方法的区别就是,当队列中没有元素时,不是直接返回null,而是调用notEmpty.await()
将当前线程挂起并放入消费者等待队列中,当队列中有元素时,它将被唤醒。
总结
LinkedBlockingQueue是使用单向链表结构实现的线程安全的阻塞队列,它可以是有界队列,也可以是无界队列。和ArrayBlockingQueue不同的是,LinkedBlockingQueue生产者和消费者各自竞争的是不同的锁,所以队列的生产和消费可以同时进行的,所以理论上,LinkedBlockingQueue的并发性能会稍好一些。
两把锁,对应的会有两个等待队列Condition。队列空时,消费者线程会被挂起并放入notEmpty。队列满时,生产者线程会被挂起并放入notFull。
当使用无界队列时,要格外小心消息堆积导致的内存溢出。