Java 并发编程:BlockingQueue vs wait/notify 详解
在并发编程中,生产者-消费者模式 是一个经典模式,日志异步写入,请求通过队列进行削峰填谷,任务拆解下发多个工作线程执行 等等场景 都非常适合这个模式来解决。
生产者不断地往缓冲区放数据,消费者不断地从缓冲区取数据。如何保证线程安全、同步有序,就是核心问题。
Java 提供了两种常见解决方案:
- 使用
wait()和notify()进行线程通信。 - 使用
BlockingQueue来简化并发控制。
使用 wait() 和 notify()
这是最原始、最底层的方式,通过 Object 类自带的监视器方法来实现线程间通信。
import java.util.LinkedList;
import java.util.Queue;
public class WaitNotifyExample {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
public void produce() throws InterruptedException {
synchronized (queue) {
while (queue.size() == CAPACITY) {
System.out.println("队列已满,生产者等待...");
queue.wait();
}
int value = (int) (Math.random() * 100);
queue.offer(value);
System.out.println("生产者生产: " + value);
queue.notifyAll(); // 通知消费者
}
}
public void consume() throws InterruptedException {
synchronized (queue) {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
queue.wait();
}
int value = queue.poll();
System.out.println("消费者消费: " + value);
queue.notifyAll(); // 通知生产者
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Runnable producer = () -> {
try {
while (true) example.produce();
} catch (InterruptedException e) { e.printStackTrace(); }
};
Runnable consumer = () -> {
try {
while (true) example.consume();
} catch (InterruptedException e) { e.printStackTrace(); }
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
特点
灵活、底层,完全可控,更能体现一个程序员的基本功和专业性,同时可以炫技。
但容易写出 Bug,容易翻车,对程序员的编码能力和经验要求高,比如忘记 notifyAll()、死锁、虚假唤醒等。
更适合 教学、理解线程通信原理。
使用 BlockingQueue
BlockingQueue 是 java.util.concurrent 包提供的并发容器,内部实现了 自动阻塞和唤醒机制。对新手更加优好,简单两个api take()和put()就能完成上面的代码架子。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
public void produce() throws InterruptedException {
int value = (int) (Math.random() * 100);
queue.put(value); // 队列满时自动阻塞
System.out.println("生产者生产: " + value);
}
public void consume() throws InterruptedException {
int value = queue.take(); // 队列空时自动阻塞
System.out.println("消费者消费: " + value);
}
public static void main(String[] args) {
BlockingQueueExample example = new BlockingQueueExample();
Runnable producer = () -> {
try {
while (true) example.produce();
} catch (InterruptedException e) { e.printStackTrace(); }
};
Runnable consumer = () -> {
try {
while (true) example.consume();
} catch (InterruptedException e) { e.printStackTrace(); }
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
特点
使用了BlockingQueue的ArrayBlockingQueue实现
ArrayBlockingQueue内部基于 ReentrantLock + Condition 实现。
当队列满时,put() 会阻塞;当队列空时,take() 会阻塞,。
自动处理线程间的等待与唤醒,避免手动管理 wait()/notify()。
使用更加简单,让程序员能够更加便捷的开发生产者消费者模式的代码
BlockingQueue
BlockingQueue 是一个接口,它的实现类(如 ArrayBlockingQueue、LinkedBlockingQueue)底层依赖:
ReentrantLock:保证队列操作的互斥。
Condition 条件变量:
notFull → 队列满时,生产者等待;
notEmpty → 队列空时,消费者等待。
队列的操作都是通过ReentrantLock进行加解锁来控制并发
可以看下ArrayBlockingQueue的实现
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
阻塞与唤醒逻辑
生产者调用 put() 时,如果队列已满:
→ 调用 notFull.await() 阻塞,直到有消费者消费数据。
消费者调用 take() 时,如果队列为空:
→ 调用 notEmpty.await() 阻塞,直到有生产者放入数据。
消费或生产完成后,会 signal() 唤醒对应的等待线程。
BlockingQueue常见实现类
| 实现类 | 特点 | 使用场景 |
|---|---|---|
| ArrayBlockingQueue | 基于数组,有界,FIFO,容量固定 | 固定任务队列,如线程池任务缓存 |
| LinkedBlockingQueue | 基于链表,容量可设,吞吐量高 | 生产快/消费慢的场景,如日志处理 |
| PriorityBlockingQueue | 支持优先级排序,无界 | 定时任务、优先级队列 |
| DelayQueue | 元素到期才能取出,基于优先级队列 | 定时任务、订单超时、缓存过期 |
| SynchronousQueue | 不存储元素,必须直接交付 | 常用于 Executors.newCachedThreadPool() |
| LinkedTransferQueue | 高并发无界队列,支持直接传递 | 高频任务传递、消息处理 |
BlockingQueue常用方法区别
我们已ArrayBlockQueue具体实现为例子
一般我们使用take()和put(),这两个具有阻塞性,使用的是lock.lockInterruptibly();来控制
offer()和poll 使用的是lock.lock();, 拿不到锁资源就返回成功或者失败
add()和无参数的remove()方法,是继承AbstractQueue,错误的情况会抛出异常
具体对比如下表
| 分类 | 方法 | 队列满/空时行为 | 是否阻塞 | 返回值/异常 |
|---|---|---|---|---|
| 插入元素 | add(e) | 满时抛 IllegalStateException | 否 | 成功 true,失败抛异常 |
offer(e) | 满时返回 false | 否 | true/false | |
offer(e, timeout, unit) | 满时等待指定时间,超时返回 false | ⏳(有限阻塞) | true/false | |
put(e) | 满时一直阻塞,直到有空间 | 是 | 无返回值 | |
| 获取元素 | remove() | 空时抛 NoSuchElementException | 否 | 元素 / 异常 |
poll() | 空时返回 null | 否 | 元素 / null | |
poll(timeout, unit) | 空时等待指定时间,超时返回 null | 有限阻塞 | 元素 / null | |
take() | 空时一直阻塞,直到有元素 | 是 | 元素 | |
| 查看队头 | element() | 空时抛 NoSuchElementException | 否 | 元素 / 异常 |
peek() | 空时返回 null | 否 | 元素 / null |
日常开发中的应用场景
在实际开发中,BlockingQueue 使用非常普遍,尤其是 异步任务处理、消息队列、线程池 中。
日志异步写入
BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(1000);
// 生产日志
public void log(String msg) throws InterruptedException {
logQueue.put(msg);
}
// 消费日志(写文件/写数据库)
public void startLogWorker() {
new Thread(() -> {
try {
while (true) {
String msg = logQueue.take();
System.out.println("写日志: " + msg);
}
} catch (InterruptedException ignored) {}
}).start();
}
避免主线程阻塞,日志异步落盘。
订单处理系统
- 生产者:接收用户订单请求,放入
BlockingQueue。 - 消费者:多个工作线程从队列取订单,进行支付/库存/通知等操作。
BlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>(500);
public void submitOrder(Order order) throws InterruptedException {
orderQueue.put(order);
}
public void startOrderWorkers() {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
while (true) {
Order order = orderQueue.take();
processOrder(order);
}
} catch (InterruptedException ignored) {}
}).start();
}
}
实现了 异步解耦,同时支持 多消费者并行处理。
对比总结
| 特性 | wait()/notify() | BlockingQueue |
|---|---|---|
| 复杂度 | 高,需要手动控制 | 低,内置阻塞机制 |
| 安全性 | 容易遗漏信号、死锁 | 线程安全,内部保证 |
| 可读性 | 代码冗长,难维护 | 简洁清晰,易扩展 |
| 使用场景 | 学习、定制化同步 | 实际开发首选 |
结论
学习阶段:掌握 wait()/notify(),有助于理解底层线程通信原理,基础不牢,地动山摇。
实际开发:推荐使用 BlockingQueue,避免重复造轮子,更安全高效。
根据场景选择不同实现类(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等),能很好地解决日常业务问题。
1299

被折叠的 条评论
为什么被折叠?



