Java 多线程核心技术:线程间通信三种经典方式详解与实战

一、为什么需要线程间通信?

大家好!今天我们来聊聊多线程编程中的一个核心问题:线程间通信

想象一下这个场景:你开发了一个电商系统,一个线程负责接收用户下单请求,另一个线程负责库存扣减,还有一个线程负责发送通知。这些线程之间如果无法协作,就像各自为战的士兵,无法完成统一的任务。

线程间通信解决的核心问题是:

  • 线程协作:多个线程按照预定的顺序执行任务
  • 数据共享:一个线程产生的数据,需要被另一个线程使用
  • 状态同步:一个线程的状态变化需要通知其他线程

Java 提供了多种线程间通信机制,今天我们重点介绍三种经典方式:

graph TD A[Java线程间通信机制] --> B[wait/notify机制] A --> C[Condition条件机制] A --> D[管道通信] A --> E[volatile变量通信] B --> F[Object类的方法] C --> G[ReentrantLock的配套工具] D --> H[PipedInputStream/PipedOutputStream] E --> I[可见性保证]

二、第一种:wait/notify 机制

2.1 核心原理

wait/notify 是 Java 最基础的线程间通信机制,它们是 Object 类的方法,而不是 Thread 类的方法。这意味着任何对象都可以作为线程间通信的媒介

基本工作原理如下:

sequenceDiagram participant 线程A participant 共享对象 participant 线程B 线程A->>共享对象: 获取锁(synchronized) 线程A->>共享对象: 检查条件(while循环) 线程A->>共享对象: wait()释放锁并进入等待状态 Note over 线程A,共享对象: 线程A释放锁,进入对象的等待队列 线程B->>共享对象: 获取锁(synchronized) 线程B->>共享对象: 修改状态 线程B->>共享对象: notify()/notifyAll()通知等待线程 Note over 线程B,共享对象: 线程B通知后继续持有锁直到同步块结束 线程B->>共享对象: 释放锁 Note over 线程A,共享对象: 线程A被唤醒,重新获取锁 共享对象-->>线程A: 重新获取锁 线程A->>共享对象: 再次检查条件(防止虚假唤醒) 线程A->>共享对象: 条件满足,继续执行

2.2 核心方法说明

  • wait(): 让当前线程进入等待状态,并释放对象锁
  • wait(long timeout): 带超时时间的等待
  • wait(long timeout, int nanos): 更精细的超时控制
  • notify(): 随机唤醒一个在该对象上等待的线程
  • notifyAll(): 唤醒所有在该对象上等待的线程

2.3 使用规则

使用 wait/notify 有一些必须遵守的规则,否则会抛出 IllegalMonitorStateException 异常:

  1. 必须在 synchronized 同步块或方法中调用
  2. 必须是同一个监视器对象
  3. wait 后必须使用循环检查等待条件(避免虚假唤醒)

关于虚假唤醒,Java 官方文档明确指出:

"线程可能在没有被通知、中断或超时的情况下被唤醒,这被称为虚假唤醒。虽然这在实际中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防范它,并且如果条件不满足则继续等待。换句话说,等待应该总是发生在循环中。"

这是由操作系统线程调度机制决定的,不是 Java 的 bug。

2.4 生产者-消费者示例

下面是一个典型的生产者-消费者模式示例,通过 wait/notify 实现线程间协作:

java 复制代码
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()?这是线程间通信中的重要决策:

  1. 使用 notify()的情况

    • 所有等待线程都是同质的(做相同任务)
    • 单一消费者/生产者模式
    • 性能敏感,且能确保不会导致线程饥饿
  2. 使用 notifyAll()的情况

    • 有多种不同类型的等待线程
    • 多生产者/多消费者模式
    • 安全性要求高于性能要求
    • 当不确定使用哪个更合适时(默认选择)

2.6 常见问题与解决方案

  1. 虚假唤醒问题

    问题:线程可能在没有 notify/notifyAll 调用的情况下被唤醒

    解决:始终使用 while 循环检查等待条件,而不是 if 语句

  2. 死锁风险

    如果生产者和消费者互相等待对方的通知,且都没有收到通知,就会发生死锁。可以考虑使用带超时参数的 wait(timeout)方法,例如:

    java 复制代码
    // 超时等待,避免永久死锁
    if (!condition) {
        this.wait(1000); // 最多等待1秒
    }
  3. 异常处理

    wait()方法会抛出 InterruptedException,需要适当处理:

    java 复制代码
    try {
        while (!condition) {
            object.wait();
        }
    } catch (InterruptedException e) {
        // 恢复中断状态,不吞掉中断
        Thread.currentThread().interrupt();
        // 或者进行资源清理并提前返回
        return;
    }

三、第二种:Condition 条件变量

3.1 基本概念

Condition 是在 Java 5 引入的,它提供了比 wait/notify 更加灵活和精确的线程间控制机制。Condition 对象总是与 Lock 对象一起使用。

graph TD A[ReentrantLock] -->|创建| B[Condition] B -->|提供| C[await方法:等待] B -->|提供| D[signal方法:通知] B -->|提供| E[signalAll方法:通知所有] F[等待队列1] --- B G[等待队列2] --- B H[等待队列3] --- B F --- I[精确唤醒] G --- I H --- I

3.2 Condition 接口的核心方法

  • await(): 类似于 wait(),释放锁并等待
  • await(long time, TimeUnit unit): 带超时的等待
  • awaitUninterruptibly(): 不可中断的等待
  • awaitUntil(Date deadline): 等待到指定的时间点
  • signal(): 类似于 notify(),唤醒一个等待线程
  • signalAll(): 类似于 notifyAll(),唤醒所有等待线程

3.3 分组唤醒原理

Condition 的核心优势在于实现"精确通知"。与 wait/notify 使用同一个等待队列不同,每个 Condition 对象管理着各自独立的等待队列

less 复制代码
wait/notify: 所有线程在同一个等待队列
┌─────────────────────┐
│ 对象监视器等待队列    │
├─────────┬───────────┤
│ 线程A    │ 线程B     │
└─────────┴───────────┘

Condition: 每个Condition维护独立的等待队列
┌─────────────────────┐  ┌─────────────────────┐
│ Condition1等待队列   │  │ Condition2等待队列   │
├─────────┬───────────┤  ├─────────┬───────────┤
│ 线程A    │ 线程C     │  │ 线程B    │ 线程D     │
└─────────┴───────────┘  └─────────┴───────────┘

这种机制使得:

  • 生产者可以只唤醒消费者(而不是所有等待线程)
  • 清空操作可以只唤醒生产者(而不是消费者)
  • 不同类型的等待可以使用不同的 Condition

3.4 相比 wait/notify 的优势

  1. 可以精确唤醒指定线程组:一个 Lock 可以创建多个 Condition 对象,实现分组唤醒
  2. 有更好的中断控制:提供可中断和不可中断的等待
  3. 可以设置超时时间:更灵活的超时机制(支持时间单位)
  4. 可以实现公平锁:使用 ReentrantLock 的公平性特性
  5. 通过独立等待队列实现精准唤醒:仅通知目标线程组,避免唤醒无关线程(如生产者不唤醒其他生产者),从而减少 CPU 资源浪费

3.5 精确通知示例

下面是一个使用 Condition 实现的生产者-消费者模式,支持精确通知:

java 复制代码
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 实现更复杂的场景,比如一个缓冲区,有读者、写者和清理者三种角色:

java 复制代码
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 的精确通知机制,仍然需要使用循环检查等待条件:

java 复制代码
// 正确使用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

这些类构成了线程之间的管道通信通道,一个线程向管道写入数据,另一个线程从管道读取数据。

graph LR A[线程A] -->|写入| B[PipedOutputStream] B -->|连接| C[PipedInputStream] C -->|读取| D[线程B] E[线程C] -->|写入| F[PipedWriter] F -->|连接| G[PipedReader] G -->|读取| H[线程D]

4.2 管道通信的核心特性

  1. 阻塞机制

    • 管道缓冲区满时,write()操作会阻塞
    • 管道缓冲区空时,read()操作会阻塞
    • 这一特性自动实现了生产者-消费者模式的流控制
  2. 字节流与字符流

    • 字节流:PipedInputStream/PipedOutputStream - 处理二进制数据
    • 字符流:PipedReader/PipedWriter - 处理文本数据(带字符编码)
  3. 内部缓冲区

    • 默认大小为 1024 字节
    • 可以在构造函数中指定缓冲区大小

4.3 管道通信的使用场景

管道通信特别适合于:

  • 需要传输原始数据或字符流的场景
  • 生产者-消费者模式中的数据传输
  • 多个处理阶段之间的流水线处理
  • 日志记录器、数据过滤、实时数据处理

4.4 字节管道示例

下面是一个使用 PipedInputStream 和 PipedOutputStream 的示例:

java 复制代码
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 的字符流管道示例:

java 复制代码
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 管道通信的注意事项

  1. 管道流容量有限

    • 默认容量为 1024 字节
    • 写入过多数据而没有及时读取,写入方会阻塞
    • 读取空管道时,读取方会阻塞
  2. 连接机制

    • 使用前必须先调用 connect()方法连接两个流,或在构造时指定
    • 多次 connect 会抛出异常
  3. 单向通信

    • 管道是单向的,需要双向通信时需要创建两对管道
    • 分清楚谁是生产者(写入方),谁是消费者(读取方)
  4. 关闭管理

    • 必须正确关闭管道流(在 finally 块中)
    • 一端关闭后另一端会收到-1 或 null,表示流结束
  5. 线程安全性

    • 单个管道的写入/读取操作是线程安全的(即单个写入线程和单个读取线程无需额外同步)
    • 多个线程同时写入/读取同一个管道仍需外部同步
    • 管道不支持多写多读模式,设计上就是一个线程写,一个线程读

五、辅助通信方式:volatile 变量

5.1 volatile 基本原理

volatile 是 Java 提供的轻量级线程间通信机制,它保证了变量的可见性有序性,但不保证原子性。

graph LR A[线程A] -->|写入| B[volatile变量] B -->|立即刷新到主内存| C[主内存] C -->|其他线程立即可见| D[线程B] D -->|读取最新值| B

5.2 volatile 的实现原理

  1. 可见性保证

    • volatile 变量的写操作会强制刷新到主内存
    • volatile 变量的读操作会强制从主内存获取最新值
    • 保证一个线程对变量的修改对其他线程立即可见
  2. 内存屏障

    • volatile 变量的读写操作会插入内存屏障指令,禁止指令重排序
    • 保证程序执行的有序性,防止编译器和 CPU 的优化破坏并发安全
    • 在 x86 架构上,写操作会生成锁前缀指令(LOCK prefix)
  3. 无锁机制

    • 不会导致线程阻塞
    • 比 synchronized 更轻量级,性能更好
    • 适合一写多读场景

5.3 volatile 与原子性

volatile 不保证原子性,这意味着:

java 复制代码
// 以下操作在多线程环境中不安全,即使counter是volatile
volatile int counter = 0;
counter++; // 非原子操作:读取-修改-写入

对于需要原子性的场景,可以结合原子类使用:

java 复制代码
// 使用原子类保证原子性
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作

或者使用 synchronized:

java 复制代码
volatile int counter = 0;
synchronized void increment() {
    counter++; // 在同步块中安全
}

5.4 使用 volatile 实现线程间通信示例

下面是一个使用 volatile 变量实现线程间通信的简单例子:

java 复制代码
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 内部同步 无锁
通信方向 多向 多向 单向 多向
通知精确性 不精确 精确 不适用 不适用
适用数据类型 任意对象 任意对象 支持连续的二进制数据或文本数据,适合流式处理(如日志、文件内容),不适合离散的对象传输 基本类型/对象引用

七、线程间通信实战案例:日志收集器

下面是一个综合应用案例,实现一个简单的日志收集器:

java 复制代码
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();
    }
}

这个案例综合使用了多种线程间通信方式:

  1. BlockingQueue: 作为线程安全的日志队列,自动实现生产者-消费者模式
  2. Piped 流: 将处理后的日志传输到输出线程
  3. 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 常见问题解答

  1. wait()和 sleep()的区别是什么?

    • wait()释放锁,sleep()不释放锁
    • wait()需要在 synchronized 块中调用,sleep()不需要
    • wait()需要被 notify()/notifyAll()唤醒,sleep()时间到自动恢复
    • wait()是 Object 类方法,sleep()是 Thread 类方法
  2. 为什么 wait()需要在 synchronized 块中调用?

    • 确保线程在检查条件和调用 wait()期间持有锁,避免竞态条件
    • 调用 wait()前必须获得对象的监视器锁,这是 JVM 层面的要求
    • 确保线程放弃锁并进入等待状态的操作是原子的
  3. 如何处理虚假唤醒问题?

    java 复制代码
    // 正确做法:使用while循环
    synchronized (obj) {
        while (!condition) {  // 循环检查
            obj.wait();
        }
        // 处理条件满足情况
    }
    
    // 错误做法:使用if语句
    synchronized (obj) {
        if (!condition) {     // 只检查一次
            obj.wait();
        }
        // 可能在条件仍不满足时执行
    }
  4. Condition 相比 wait/notify 的优势在哪里?

    • 可以创建多个等待队列,实现精确通知
    • 可以实现不可中断的等待(awaitUninterruptibly)
    • 支持更灵活的超时控制(可指定时间单位)
    • 与 ReentrantLock 结合可实现公平锁
  5. 如何选择合适的线程间通信方式?

    • 简单状态同步:volatile 变量
    • 一个等待条件:wait/notify
    • 多个等待条件:Condition
    • 数据流传输:管道通信
    • 队列操作:BlockingQueue
  6. volatile 与 AtomicInteger 的区别?

    • volatile 只保证可见性和有序性,不保证原子性
    • AtomicInteger 通过 CAS(Compare-And-Swap)操作保证原子性
    • 对于 i++ 这样的操作,需要使用 AtomicInteger 而非 volatile
    • 两者结合使用可以实现高效的线程安全代码
  7. 管道通信与消息队列有什么区别?

    • 管道是 Java 内置的线程间通信机制,限于单 JVM 内
    • 消息队列通常指分布式消息系统(如 Kafka),可跨进程/服务器
    • 管道适合轻量级的线程间流数据传输
    • 消息队列适合更大规模的分布式系统组件间通信

感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~

相关推荐
LTPP3 分钟前
掌握Rust Web开发的未来:Hyperlane框架全方位教程 🎓🔧
前端·后端·github
LemonDus23 分钟前
Cursor入门教程-JetBrains过度向
后端·工具·技术知识
2401_8454174527 分钟前
C++ string类
java·开发语言·c++
涡能增压发动积29 分钟前
SpringAI+LiteFlow实现智能体编排
人工智能·后端
精神内耗中的钙奶饼干29 分钟前
Windows 系统搭建Kafka集群记录
后端·kafka
uhakadotcom38 分钟前
Apache APISIX入门指南:快速理解与实战示例
后端·面试·github
wuqingshun3141591 小时前
经典算法 判断一个图中是否有环
java·开发语言·数据结构·c++·算法·蓝桥杯·深度优先
神仙别闹1 小时前
基于JSP+MySQL实现用户注册登录及短信发送功能
java·开发语言·mysql
code喵喵1 小时前
架构设计系列
java
郝同学的测开笔记1 小时前
云原生探索系列(十五):Go 语言通道
后端·云原生·go