题目
实现一个线程安全的阻塞队列并解释其工作原理
信息
- 类型:问答
- 难度:⭐⭐
考点
线程安全设计,锁机制,生产者-消费者模型,条件变量
快速回答
实现线程安全阻塞队列的核心要点:
- 使用
ReentrantLock保证操作原子性 - 通过
Condition实现精确的线程等待/唤醒机制 - 维护两个条件变量:
notEmpty(非空)和notFull(非满) - 数组循环存储实现队列数据结构
- 在
put()/take()方法中正确处理边界条件
原理说明
阻塞队列的核心是生产者-消费者模型,当队列满时生产者线程阻塞,队列空时消费者线程阻塞。Java并发包通过ReentrantLock和Condition实现:
- 锁机制:
ReentrantLock保证put/take操作的原子性 - 条件变量:
Condition实现精确的线程调度,比wait()/notify()更灵活 - 循环数组:使用定长数组实现队列,通过头尾指针循环利用空间
代码示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BlockingQueue<T> {
private final T[] items;
private int count;
private int putIndex;
private int takeIndex;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BlockingQueue(int capacity) {
items = (T[]) new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 队列满时阻塞生产者
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0; // 循环指针
count++;
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 队列空时阻塞消费者
}
T item = items[takeIndex];
items[takeIndex] = null; // 帮助GC
if (++takeIndex == items.length) takeIndex = 0; // 循环指针
count--;
notFull.signal(); // 唤醒生产者
return item;
} finally {
lock.unlock();
}
}
}最佳实践
- 双条件变量:分离空/满两种等待条件,避免无效唤醒
- 循环数组:固定容量避免内存溢出,适合有界队列场景
- 锁释放:务必在
finally块中释放锁防止死锁 - 中断处理:方法声明抛出
InterruptedException支持响应中断
常见错误
- 单条件变量:使用单个
Condition会导致无效唤醒和性能下降 - 未循环指针:忘记重置指针导致数组越界
- 非线程安全操作:直接使用
LinkedList等非线程安全集合 - 锁未释放:异常路径中未释放锁造成死锁
扩展知识
- Java内置实现:
ArrayBlockingQueue(有界)、LinkedBlockingQueue(可选有界) - 锁优化:
ReentrantLock支持公平/非公平锁,默认非公平锁吞吐量更高 - 性能对比:
ConcurrentLinkedQueue使用CAS实现无锁队列,适合超高并发但非阻塞场景 - 条件谓词:
while循环检查条件防止虚假唤醒(spurious wakeup)