一、为什么需要线程间通信?
大家好!今天我们来聊聊多线程编程中的一个核心问题:线程间通信。
想象一下这个场景:你开发了一个电商系统,一个线程负责接收用户下单请求,另一个线程负责库存扣减,还有一个线程负责发送通知。这些线程之间如果无法协作,就像各自为战的士兵,无法完成统一的任务。
线程间通信解决的核心问题是:
- 线程协作:多个线程按照预定的顺序执行任务
- 数据共享:一个线程产生的数据,需要被另一个线程使用
- 状态同步:一个线程的状态变化需要通知其他线程
Java 提供了多种线程间通信机制,今天我们重点介绍三种经典方式:
二、第一种:wait/notify 机制
2.1 核心原理
wait/notify 是 Java 最基础的线程间通信机制,它们是 Object 类的方法,而不是 Thread 类的方法。这意味着任何对象都可以作为线程间通信的媒介。
基本工作原理如下:
2.2 核心方法说明
- wait(): 让当前线程进入等待状态,并释放对象锁
- wait(long timeout): 带超时时间的等待
- wait(long timeout, int nanos): 更精细的超时控制
- notify(): 随机唤醒一个在该对象上等待的线程
- notifyAll(): 唤醒所有在该对象上等待的线程
2.3 使用规则
使用 wait/notify 有一些必须遵守的规则,否则会抛出 IllegalMonitorStateException 异常:
- 必须在 synchronized 同步块或方法中调用
- 必须是同一个监视器对象
- wait 后必须使用循环检查等待条件(避免虚假唤醒)
关于虚假唤醒,Java 官方文档明确指出:
"线程可能在没有被通知、中断或超时的情况下被唤醒,这被称为虚假唤醒。虽然这在实际中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防范它,并且如果条件不满足则继续等待。换句话说,等待应该总是发生在循环中。"
这是由操作系统线程调度机制决定的,不是 Java 的 bug。
2.4 生产者-消费者示例
下面是一个典型的生产者-消费者模式示例,通过 wait/notify 实现线程间协作:
public class WaitNotifyExample {
private final Queue<String> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
public synchronized void produce(String data) throws InterruptedException {
// 使用while循环检查条件,防止虚假唤醒
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
this.wait(); // 队列满了,生产者线程等待
}
queue.add(data);
System.out.println("生产数据: " + data + ", 当前队列大小: " + queue.size());
// 只通知消费者线程,避免不必要的唤醒
this.notify(); // 在单生产者单消费者的情况下可以用notify提高效率
}
public synchronized String consume() throws InterruptedException {
// 使用while循环检查条件,防止虚假唤醒
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
this.wait(); // 队列空了,消费者线程等待
}
String data = queue.poll();
System.out.println("消费数据: " + data + ", 当前队列大小: " + queue.size());
// 只通知生产者线程,避免不必要的唤醒
this.notify(); // 在单生产者单消费者的情况下可以用notify提高效率
return data;
}
// 对于多生产者多消费者的场景,应改用notifyAll避免线程饥饿
public synchronized void produceMulti(String data) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
System.out.println(Thread.currentThread().getName() + ": 队列已满,生产者等待...");
this.wait();
}
queue.add(data);
System.out.println(Thread.currentThread().getName() + ": 生产数据: " + data + ", 当前队列大小: " + queue.size());
// 当有多个生产者和消费者时,必须用notifyAll确保正确唤醒
this.notifyAll();
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
// 创建生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
example.produce("数据-" + i);
Thread.sleep(new Random().nextInt(1000));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
example.consume();
Thread.sleep(new Random().nextInt(1000));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
2.5 notify()与 notifyAll()的选择策略
何时使用 notify(),何时使用 notifyAll()?这是线程间通信中的重要决策:
使用 notify()的情况:
- 所有等待线程都是同质的(做相同任务)
- 单一消费者/生产者模式
- 性能敏感,且能确保不会导致线程饥饿
使用 notifyAll()的情况:
- 有多种不同类型的等待线程
- 多生产者/多消费者模式
- 安全性要求高于性能要求
- 当不确定使用哪个更合适时(默认选择)
2.6 常见问题与解决方案
虚假唤醒问题
问题:线程可能在没有 notify/notifyAll 调用的情况下被唤醒
解决:始终使用 while 循环检查等待条件,而不是 if 语句
死锁风险
如果生产者和消费者互相等待对方的通知,且都没有收到通知,就会发生死锁。可以考虑使用带超时参数的 wait(timeout)方法,例如:
// 超时等待,避免永久死锁 if (!condition) { this.wait(1000); // 最多等待1秒 }
异常处理
wait()方法会抛出 InterruptedException,需要适当处理:
try { while (!condition) { object.wait(); } } catch (InterruptedException e) { // 恢复中断状态,不吞掉中断 Thread.currentThread().interrupt(); // 或者进行资源清理并提前返回 return; }
三、第二种:Condition 条件变量
3.1 基本概念
Condition 是在 Java 5 引入的,它提供了比 wait/notify 更加灵活和精确的线程间控制机制。Condition 对象总是与 Lock 对象一起使用。
3.2 Condition 接口的核心方法
- await(): 类似于 wait(),释放锁并等待
- await(long time, TimeUnit unit): 带超时的等待
- awaitUninterruptibly(): 不可中断的等待
- awaitUntil(Date deadline): 等待到指定的时间点
- signal(): 类似于 notify(),唤醒一个等待线程
- signalAll(): 类似于 notifyAll(),唤醒所有等待线程
3.3 分组唤醒原理
Condition 的核心优势在于实现"精确通知"。与 wait/notify 使用同一个等待队列不同,每个 Condition 对象管理着各自独立的等待队列。
wait/notify: 所有线程在同一个等待队列
┌─────────────────────┐
│ 对象监视器等待队列 │
├─────────┬───────────┤
│ 线程A │ 线程B │
└─────────┴───────────┘
Condition: 每个Condition维护独立的等待队列
┌─────────────────────┐ ┌─────────────────────┐
│ Condition1等待队列 │ │ Condition2等待队列 │
├─────────┬───────────┤ ├─────────┬───────────┤
│ 线程A │ 线程C │ │ 线程B │ 线程D │
└─────────┴───────────┘ └─────────┴───────────┘
这种机制使得:
- 生产者可以只唤醒消费者(而不是所有等待线程)
- 清空操作可以只唤醒生产者(而不是消费者)
- 不同类型的等待可以使用不同的 Condition
3.4 相比 wait/notify 的优势
- 可以精确唤醒指定线程组:一个 Lock 可以创建多个 Condition 对象,实现分组唤醒
- 有更好的中断控制:提供可中断和不可中断的等待
- 可以设置超时时间:更灵活的超时机制(支持时间单位)
- 可以实现公平锁:使用 ReentrantLock 的公平性特性
- 通过独立等待队列实现精准唤醒:仅通知目标线程组,避免唤醒无关线程(如生产者不唤醒其他生产者),从而减少 CPU 资源浪费
3.5 精确通知示例
下面是一个使用 Condition 实现的生产者-消费者模式,支持精确通知:
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列不满条件
private final Condition notEmpty = lock.newCondition(); // 队列不空条件
private final Queue<String> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
public void produce(String data) throws InterruptedException {
lock.lock();
try {
// 队列已满,等待不满条件
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
notFull.await(); // 生产者在notFull条件上等待
}
queue.add(data);
System.out.println("生产数据: " + data + ", 当前队列大小: " + queue.size());
// 通知消费者队列不为空 - 精确通知,只唤醒消费者线程
notEmpty.signal();
} finally {
// 必须在finally中释放锁,确保锁一定被释放
lock.unlock();
}
}
public String consume() throws InterruptedException {
lock.lock();
try {
// 队列为空,等待不空条件
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await(); // 消费者在notEmpty条件上等待
}
String data = queue.poll();
System.out.println("消费数据: " + data + ", 当前队列大小: " + queue.size());
// 通知生产者队列不满 - 精确通知,只唤醒生产者线程
notFull.signal();
return data;
} finally {
lock.unlock();
}
}
// 使用可中断锁尝试获取数据,带超时控制
public String consumeWithTimeout(long timeout, TimeUnit unit) throws InterruptedException {
// 尝试获取锁,可设置超时
if (!lock.tryLock(timeout, unit)) {
System.out.println("获取锁超时,放弃消费");
return null;
}
try {
// 使用超时等待
if (queue.isEmpty() && !notEmpty.await(timeout, unit)) {
System.out.println("等待数据超时,放弃消费");
return null;
}
if (!queue.isEmpty()) {
String data = queue.poll();
System.out.println("消费数据: " + data + ", 当前队列大小: " + queue.size());
notFull.signal();
return data;
}
return null;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionExample example = new ConditionExample();
// 创建生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
example.produce("数据-" + i);
Thread.sleep(new Random().nextInt(1000));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
example.consume();
Thread.sleep(new Random().nextInt(1000));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
3.6 使用 Condition 实现多条件协作
我们可以使用多个 Condition 实现更复杂的场景,比如一个缓冲区,有读者、写者和清理者三种角色:
public class MultiConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition writerCondition = lock.newCondition(); // 写入条件
private final Condition readerCondition = lock.newCondition(); // 读取条件
private final Condition cleanerCondition = lock.newCondition(); // 清理条件
private boolean hasData = false;
private boolean needClean = false;
private String data;
// 写入数据
public void write(String data) throws InterruptedException {
lock.lock();
try {
// 已有数据或需要清理,等待
while (hasData || needClean) {
System.out.println(Thread.currentThread().getName() + " 等待写入条件...");
writerCondition.await();
}
this.data = data;
hasData = true;
System.out.println(Thread.currentThread().getName() + " 写入数据: " + data);
// 通知读者可以读取数据
readerCondition.signal();
} finally {
lock.unlock();
}
}
// 读取数据
public String read() throws InterruptedException {
lock.lock();
try {
// 没有数据或需要清理,等待
while (!hasData || needClean) {
System.out.println(Thread.currentThread().getName() + " 等待读取条件...");
readerCondition.await();
}
String result = this.data;
hasData = false;
needClean = true;
System.out.println(Thread.currentThread().getName() + " 读取数据: " + result);
// 通知清理者可以清理
cleanerCondition.signal();
return result;
} finally {
lock.unlock();
}
}
// 清理操作
public void clean() throws InterruptedException {
lock.lock();
try {
// 不需要清理,等待
while (!needClean) {
System.out.println(Thread.currentThread().getName() + " 等待清理条件...");
cleanerCondition.await();
}
this.data = null;
needClean = false;
System.out.println(Thread.currentThread().getName() + " 清理完成");
// 通知写者可以写入数据
writerCondition.signal();
} finally {
lock.unlock();
}
}
// 测试方法
public static void main(String[] args) {
MultiConditionExample example = new MultiConditionExample();
// 写入线程
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
example.write("数据-" + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "写入线程").start();
// 读取线程
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
example.read();
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "读取线程").start();
// 清理线程
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
example.clean();
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "清理线程").start();
}
}
3.7 Condition 与虚假唤醒
需要注意的是,Condition 的 await()方法同样会发生虚假唤醒,与 wait()方法类似。虚假唤醒是所有线程等待机制的固有特性,而不是特定通知机制的缺陷。即使使用 Condition 的精确通知机制,仍然需要使用循环检查等待条件:
// 正确使用Condition的方式
lock.lock();
try {
while (!condition) { // 使用循环检查条件
condition.await();
}
// 处理条件满足的情况
} finally {
lock.unlock();
}
Condition 虽然通过独立的等待队列减少了"无效唤醒"(非目标线程的唤醒),但无法消除操作系统层面的虚假唤醒可能性。
3.8 wait/notify 与 Condition 性能对比
虽然 Condition 在功能上更加强大,但实际性能与 wait/notify 非常接近,因为两者底层都依赖于操作系统的线程阻塞机制。两者的性能差异在高并发场景下可忽略,功能匹配度是首要考虑因素。主要区别在于:
- Condition 需要显式管理锁:Lock.lock()和 lock.unlock(),增加了代码复杂度
- Condition 提供更多控制能力:超时、中断、多条件等
- Lock 支持非阻塞尝试获取锁:tryLock()可避免长时间阻塞
选择标准:功能需求应优先于性能考虑,复杂的线程间协作场景下首选 Condition。
四、第三种:管道通信
4.1 管道通信基本概念
Java IO 提供了管道流,专门用于线程间的数据传输,适用于单 JVM 内的线程间通信。主要涉及以下几个类:
- PipedOutputStream 和 PipedInputStream
- PipedWriter 和 PipedReader
这些类构成了线程之间的管道通信通道,一个线程向管道写入数据,另一个线程从管道读取数据。
4.2 管道通信的核心特性
阻塞机制:
- 管道缓冲区满时,write()操作会阻塞
- 管道缓冲区空时,read()操作会阻塞
- 这一特性自动实现了生产者-消费者模式的流控制
字节流与字符流:
- 字节流:PipedInputStream/PipedOutputStream - 处理二进制数据
- 字符流:PipedReader/PipedWriter - 处理文本数据(带字符编码)
内部缓冲区:
- 默认大小为 1024 字节
- 可以在构造函数中指定缓冲区大小
4.3 管道通信的使用场景
管道通信特别适合于:
- 需要传输原始数据或字符流的场景
- 生产者-消费者模式中的数据传输
- 多个处理阶段之间的流水线处理
- 日志记录器、数据过滤、实时数据处理
4.4 字节管道示例
下面是一个使用 PipedInputStream 和 PipedOutputStream 的示例:
public class PipedStreamExample {
public static void main(String[] args) throws Exception {
// 创建管道输出流和输入流
PipedOutputStream output = new PipedOutputStream();
PipedInputStream input = new PipedInputStream(output); // 直接在构造器中连接
// 创建写入线程
Thread writerThread = new Thread(() -> {
try {
System.out.println("写入线程启动");
for (int i = 1; i <= 10; i++) {
String message = "数据-" + i;
output.write(message.getBytes());
System.out.println("写入: " + message);
// 如果注释掉sleep,可能会因为管道缓冲区满而阻塞
Thread.sleep(500);
}
// 关闭输出流,表示不再写入数据
output.close();
} catch (Exception e) {
e.printStackTrace();
}
});
// 创建读取线程
Thread readerThread = new Thread(() -> {
try {
System.out.println("读取线程启动");
byte[] buffer = new byte[100]; // 小于完整消息长度,演示多次读取
int len;
// read方法在管道没有数据时会阻塞,直到有数据或管道关闭
while ((len = input.read(buffer)) != -1) {
String message = new String(buffer, 0, len);
System.out.println("读取: " + message);
// 如果注释掉sleep,可能会因为消费太快而导致管道经常为空
Thread.sleep(1000);
}
input.close();
} catch (Exception e) {
e.printStackTrace();
}
});
// 启动线程
writerThread.start();
readerThread.start();
}
}
4.5 字符管道示例
下面是使用 PipedWriter 和 PipedReader 的字符流管道示例:
public class PipedReaderWriterExample {
public static void main(String[] args) throws Exception {
// 创建管道写入器和读取器
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader(writer, 1024); // 指定缓冲区大小
// 创建写入线程
Thread writerThread = new Thread(() -> {
try {
System.out.println("写入线程启动");
for (int i = 1; i <= 10; i++) {
String message = "字符数据-" + i + "\n";
writer.write(message);
writer.flush(); // 确保数据立即写入管道
System.out.println("写入: " + message);
Thread.sleep(500);
}
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
});
// 创建读取线程
Thread readerThread = new Thread(() -> {
try {
System.out.println("读取线程启动");
char[] buffer = new char[1024];
int len;
// 演示按行读取
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("读取一行: " + line);
Thread.sleep(700);
}
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
});
// 启动线程
writerThread.start();
readerThread.start();
}
}
4.6 管道通信的注意事项
管道流容量有限:
- 默认容量为 1024 字节
- 写入过多数据而没有及时读取,写入方会阻塞
- 读取空管道时,读取方会阻塞
连接机制:
- 使用前必须先调用 connect()方法连接两个流,或在构造时指定
- 多次 connect 会抛出异常
单向通信:
- 管道是单向的,需要双向通信时需要创建两对管道
- 分清楚谁是生产者(写入方),谁是消费者(读取方)
关闭管理:
- 必须正确关闭管道流(在 finally 块中)
- 一端关闭后另一端会收到-1 或 null,表示流结束
线程安全性:
- 单个管道的写入/读取操作是线程安全的(即单个写入线程和单个读取线程无需额外同步)
- 多个线程同时写入/读取同一个管道仍需外部同步
- 管道不支持多写多读模式,设计上就是一个线程写,一个线程读
五、辅助通信方式:volatile 变量
5.1 volatile 基本原理
volatile 是 Java 提供的轻量级线程间通信机制,它保证了变量的可见性和有序性,但不保证原子性。
5.2 volatile 的实现原理
可见性保证:
- volatile 变量的写操作会强制刷新到主内存
- volatile 变量的读操作会强制从主内存获取最新值
- 保证一个线程对变量的修改对其他线程立即可见
内存屏障:
- volatile 变量的读写操作会插入内存屏障指令,禁止指令重排序
- 保证程序执行的有序性,防止编译器和 CPU 的优化破坏并发安全
- 在 x86 架构上,写操作会生成锁前缀指令(LOCK prefix)
无锁机制:
- 不会导致线程阻塞
- 比 synchronized 更轻量级,性能更好
- 适合一写多读场景
5.3 volatile 与原子性
volatile 不保证原子性,这意味着:
// 以下操作在多线程环境中不安全,即使counter是volatile
volatile int counter = 0;
counter++; // 非原子操作:读取-修改-写入
对于需要原子性的场景,可以结合原子类使用:
// 使用原子类保证原子性
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作
或者使用 synchronized:
volatile int counter = 0;
synchronized void increment() {
counter++; // 在同步块中安全
}
5.4 使用 volatile 实现线程间通信示例
下面是一个使用 volatile 变量实现线程间通信的简单例子:
public class VolatileCommunicationExample {
// 使用volatile标记共享变量
private static volatile boolean flag = false;
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 创建Writer线程
Thread writerThread = new Thread(() -> {
System.out.println("写入线程开始运行");
try {
Thread.sleep(1000); // 模拟耗时操作
counter = 100; // 更新数据
// volatile变量的写操作会强制刷新到主内存
// 其他线程的读取会从主内存获取最新值
// 内存屏障确保以下操作不会被重排序到上面操作之前
flag = true; // 设置标志位
System.out.println("写入线程完成数据更新: counter = " + counter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 创建Reader线程
Thread readerThread = new Thread(() -> {
System.out.println("读取线程开始运行");
// 不使用sleep等待,volatile的可见性使其工作
while (!flag) {
// 自旋等待标志位变化
Thread.yield(); // 减少CPU占用
}
// 由于volatile的可见性保证,这里读取到的一定是最新值100
System.out.println("读取线程读取到数据: counter = " + counter);
});
// 启动线程
readerThread.start();
writerThread.start();
// 等待线程结束
writerThread.join();
readerThread.join();
}
}
5.5 volatile 的适用场景与局限性
适用场景:
- 状态标志:如开关变量、完成标志等
- 一写多读:一个线程写,多个线程读的场景
- 无原子操作:变量操作不需要保证原子性的场景
- 双重检查锁定模式:在单例模式中用于安全发布
局限性:
- 不保证原子性:对于
i++
这样的复合操作无法保证 - 不能替代锁:对于复杂共享状态的控制还是需要锁
- 性能考虑:频繁修改的变量使用 volatile 可能导致总线流量增加
六、三种通信方式的对比与选择
不同的线程间通信方式有各自的特点和适用场景,下表对比了它们的关键特性:
特性 | wait/notify | Condition | 管道通信 | volatile |
---|---|---|---|---|
线程安全级别 | 高(内置锁) | 高(显式锁) | 中(缓冲区) | 低(仅可见性) |
数据传输能力 | 通过共享对象 | 通过共享对象 | 流式传输 | 单个变量 |
适用场景 | 线程间协作 | 复杂线程间协作 | 数据流传输 | 状态标志 |
实现复杂度 | 简单 | 中等 | 中等 | 简单 |
控制精度 | 一般 | 高 | 不适用 | 不适用 |
阻塞特性 | 阻塞 | 阻塞 | 阻塞 | 非阻塞 |
锁机制 | synchronized | ReentrantLock | 内部同步 | 无锁 |
通信方向 | 多向 | 多向 | 单向 | 多向 |
通知精确性 | 不精确 | 精确 | 不适用 | 不适用 |
适用数据类型 | 任意对象 | 任意对象 | 支持连续的二进制数据或文本数据,适合流式处理(如日志、文件内容),不适合离散的对象传输 | 基本类型/对象引用 |
七、线程间通信实战案例:日志收集器
下面是一个综合应用案例,实现一个简单的日志收集器:
public class LogCollector {
// 日志队列 - 内部已实现线程安全
private final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(1000);
// 停止标志
private volatile boolean stopped = false;
// 用于管道通信的字符写入器与读取器
private PipedWriter logWriter;
private PipedReader logReader;
// 线程管理
private Thread collectorThread; // 日志收集线程
private Thread processorThread; // 日志处理线程
private Thread outputThread; // 日志输出线程
public LogCollector() throws IOException {
// 初始化管道
this.logWriter = new PipedWriter();
this.logReader = new PipedReader(logWriter);
}
public void start() {
// 创建日志收集线程
collectorThread = new Thread(() -> {
System.out.println("日志收集线程启动");
try {
while (!stopped) {
// 模拟生成日志
String log = "INFO " + new Date() + ": " + "系统运行正常,内存使用率: "
+ new Random().nextInt(100) + "%";
// BlockingQueue的put方法在队列满时会自动阻塞
logQueue.put(log);
System.out.println("收集日志: " + log);
// 控制日志生成速度
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("日志收集线程结束");
});
// 创建日志处理线程
processorThread = new Thread(() -> {
System.out.println("日志处理线程启动");
try {
while (!stopped || !logQueue.isEmpty()) {
// BlockingQueue的poll方法在队列空时会阻塞指定时间
String log = logQueue.poll(500, TimeUnit.MILLISECONDS);
if (log != null) {
// 处理日志(这里简单加上处理标记)
String processedLog = "已处理: " + log + "\n";
// 通过管道发送到输出线程
logWriter.write(processedLog);
logWriter.flush();
}
}
// 处理完所有日志后关闭写入器
logWriter.close();
} catch (InterruptedException | IOException e) {
Thread.currentThread().interrupt();
}
System.out.println("日志处理线程结束");
});
// 创建日志输出线程
outputThread = new Thread(() -> {
try {
System.out.println("日志输出线程启动");
BufferedReader reader = new BufferedReader(logReader);
String line;
// 从管道中读取处理后的日志并输出
while ((line = reader.readLine()) != null) {
System.out.println("输出处理后的日志: " + line);
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("日志输出线程结束");
});
// 启动线程
collectorThread.start();
processorThread.start();
outputThread.start();
}
public void stop() {
stopped = true; // volatile保证可见性
System.out.println("日志收集器停止中...");
}
public static void main(String[] args) throws InterruptedException, IOException {
LogCollector collector = new LogCollector();
collector.start();
// 运行10秒后停止
Thread.sleep(10000);
collector.stop();
}
}
这个案例综合使用了多种线程间通信方式:
- BlockingQueue: 作为线程安全的日志队列,自动实现生产者-消费者模式
- Piped 流: 将处理后的日志传输到输出线程
- volatile 变量: 用作停止标志控制线程终止
设计说明:这个实例展示了如何组合不同的线程间通信机制实现复杂功能:
- BlockingQueue 处理生产者-消费者数据传递(收集线程与处理线程)
- 管道通信处理字符流传输(处理线程与输出线程)
- volatile 变量处理状态同步(停止信号)
八、总结与常见问题
8.1 线程间通信方式总结
通信方式 | 核心 API | 使用场景 | 注意事项 |
---|---|---|---|
wait/notify | Object.wait() Object.notify() Object.notifyAll() | 简单同步 生产者-消费者 | 必须在 synchronized 中使用 使用 while 循环检查条件 防止虚假唤醒 |
Condition | lock.newCondition() condition.await() condition.signal() | 复杂多条件 精确通知 | 必须与 Lock 配合使用 需手动加解锁 使用 try/finally 保证锁释放 同样需防范虚假唤醒 |
管道通信 | PipedInputStream PipedOutputStream PipedReader PipedWriter | 数据传输 流处理 | 需要 connect 连接 单向通信 注意关闭资源 一写一读模式 |
volatile | volatile 关键字 | 状态标志 一写多读 | 不保证原子性 适合简单状态同步 |
8.2 常见问题解答
wait()和 sleep()的区别是什么?
- wait()释放锁,sleep()不释放锁
- wait()需要在 synchronized 块中调用,sleep()不需要
- wait()需要被 notify()/notifyAll()唤醒,sleep()时间到自动恢复
- wait()是 Object 类方法,sleep()是 Thread 类方法
为什么 wait()需要在 synchronized 块中调用?
- 确保线程在检查条件和调用 wait()期间持有锁,避免竞态条件
- 调用 wait()前必须获得对象的监视器锁,这是 JVM 层面的要求
- 确保线程放弃锁并进入等待状态的操作是原子的
如何处理虚假唤醒问题?
// 正确做法:使用while循环 synchronized (obj) { while (!condition) { // 循环检查 obj.wait(); } // 处理条件满足情况 } // 错误做法:使用if语句 synchronized (obj) { if (!condition) { // 只检查一次 obj.wait(); } // 可能在条件仍不满足时执行 }
Condition 相比 wait/notify 的优势在哪里?
- 可以创建多个等待队列,实现精确通知
- 可以实现不可中断的等待(awaitUninterruptibly)
- 支持更灵活的超时控制(可指定时间单位)
- 与 ReentrantLock 结合可实现公平锁
如何选择合适的线程间通信方式?
- 简单状态同步:volatile 变量
- 一个等待条件:wait/notify
- 多个等待条件:Condition
- 数据流传输:管道通信
- 队列操作:BlockingQueue
volatile 与 AtomicInteger 的区别?
- volatile 只保证可见性和有序性,不保证原子性
- AtomicInteger 通过 CAS(Compare-And-Swap)操作保证原子性
- 对于
i++
这样的操作,需要使用 AtomicInteger 而非 volatile - 两者结合使用可以实现高效的线程安全代码
管道通信与消息队列有什么区别?
- 管道是 Java 内置的线程间通信机制,限于单 JVM 内
- 消息队列通常指分布式消息系统(如 Kafka),可跨进程/服务器
- 管道适合轻量级的线程间流数据传输
- 消息队列适合更大规模的分布式系统组件间通信
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。