LinkedBlockingQueue
LinkedBlockingQueue 是一个基于链表的线程安全的阻塞队列:
- 可以在队列头部和尾部进行高效的插入和删除操作。
- 当队列为空时,取操作会被阻塞,直到队列中有新的元素可用。当队列已满时,插入操作会被阻塞,直到队列有可用空间。
- 可以在构造时指定最大容量。如果不指定,默认为 Integer.MAX_VALUE,这意味着队列的大小受限于可用内存。
java
/** 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();
- count: 一个 AtomicInteger,表示队列中当前元素的数量。通过原子操作保证其线程安全。
- head: 队列的头部节点。由于这是一个 FIFO 队列,所以元素总是从头部移除。头部节点的 item 字段始终为 null,它作为一个虚拟节点,用于帮助管理队列。
- last: 队列的尾部节点。新元素总是插入到尾部。
- takeLock 和 putLock: 这是 LinkedBlockingQueue 中的两把 ReentrantLock 锁。takeLock 用于控制取操作,putLock 用于控制放入操作。这样的设计使得放入和取出操作能够在一定程度上并行执行,从而提高队列的吞吐量。
- notEmpty 和 notFull: 这是两个 Condition 变量,分别与 takeLock 和 putLock 相关联。当队列为空时,尝试从队列中取出元素的线程将会在 notEmpty 上等待。当新元素被放入队列时,这些等待的线程将会被唤醒。同样地,当队列已满时,尝试向队列中放入元素的线程将会在 notFull 上等待,等待队列有可用空间时被唤醒。
链表的 Node 节点的定义如下:
java
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; }
}
01)item: 这个字段用于存储节点包含的元素。
02)next: 这个字段表示节点在队列中的后继节点。这个字段有三个可能的值:
- 后继节点的实际引用。
- 此节点自身的引用,意味着后继节点是头节点的下一个节点。
- null,表示没有后继节点,也就是说此节点是队列的最后一个节点。
03)Node(E x): 这是节点类的构造方法,它接受一个元素 x 并将其赋值给 item 字段。
1)put 方法详解
put 方法源码如下:
java
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>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//如果队列已满,则阻塞当前线程,将其移入等待队列
while (count.get() == capacity) {
notFull.await();
}
//入队操作,插入数据
enqueue(node);
c = count.getAndIncrement();
//若队列满足插入数据的条件,则通知被阻塞的生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
put 方法的逻辑基本上和 ArrayBlockingQueue 的一样。
01)参数检查:如果传入的元素为 null,则抛出 NullPointerException。LinkedBlockingQueue 不允许插入 null 元素。
02)局部变量初始化:
- int c = -1; 用于存储操作前的队列元素数量,预设为 -1 表示失败,除非稍后设置。
- Node node = new Node(e); 创建一个新的节点包含要插入的元素 e。
- final ReentrantLock putLock = this.putLock; 和 final AtomicInteger count = this.count; 获取队列的锁和计数器对象。
03)获取锁:putLock.lockInterruptibly(); 尝试获取用于插入操作的锁,如果线程被中断,则抛出 InterruptedException。
04)等待队列非满:如果队列已满(count.get() == capacity),当前线程将被阻塞,并等待 notFull 条件被满足。一旦有空间可用,线程将被唤醒继续执行。
05)入队操作:调用 enqueue(node); 将新节点插入队列的尾部。
06)更新计数:通过 c = count.getAndIncrement(); 获取并递增队列的元素计数。
07)检查并可能的唤醒其他生产者线程:如果队列没有满(c + 1 < capacity),使用 notFull.signal(); 唤醒可能正在等待插入空间的其他生产者线程。
08)释放锁:finally 块确保锁在操作完成后被释放。
09)可能的唤醒消费者线程:如果插入操作将队列从空变为非空(c == 0),则调用 signalNotEmpty(); 唤醒可能正在等待非空队列的消费者线程。
2)take 方法详解
take 方法的源码如下:
java
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)
signalNotFull();
return x;
}
01)局部变量初始化:
- E x; 用于存储被取出的元素。
- int c = -1; 用于存储操作前的队列元素数量,预设为 -1 表示失败,除非稍后设置。
- final AtomicInteger count = this.count; 和 final ReentrantLock takeLock = this.takeLock; 获取队列的计数器和锁对象。
02)获取锁:takeLock.lockInterruptibly(); 尝试获取用于取出操作的锁,如果线程被中断,则抛出 InterruptedException。
03)等待队列非空:如果队列为空(count.get() == 0),当前线程将被阻塞,并等待 notEmpty 条件被满足。一旦队列非空,线程将被唤醒继续执行。
04)出队操作:调用 x = dequeue(); 从队列的头部移除元素,并将其赋值给 x。
05)更新计数:通过 c = count.getAndDecrement(); 获取并递减队列的元素计数。
06)检查并可能的唤醒其他消费者线程:如果队列仍有其他元素(c > 1),使用 notEmpty.signal(); 唤醒可能正在等待非空队列的其他消费者线程。
07)释放锁:finally 块确保锁在操作完成后被释放。
08)可能的唤醒生产者线程:如果取出操作将队列从满变为未满(c == capacity),则调用 signalNotFull(); 唤醒可能正在等待插入空间的生产者线程。
09)返回取出的元素:最后返回被取出的元素 x。
3)使用示例
java
public class LinkedBlockingQueueTest {
private static LinkedBlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(10);
public static void main(String[] args) {
new Thread(new Producer()).start();
new Thread(new Consumer()).start();
}
static class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
blockingQueue.put(i);
System.out.println("生产者生产数据:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Integer data = blockingQueue.take();
System.out.println("消费者消费数据:" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
程序运行的部分结果如下图所示:
