头图

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

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

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

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

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

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 实现线程间协作:

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)方法,例如:

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

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

    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 对象管理着各自独立的等待队列

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 实现的生产者-消费者模式,支持精确通知:

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

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

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 的示例:

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 管道通信的注意事项

  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 不保证原子性,这意味着:

// 以下操作在多线程环境中不安全,即使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/notifyCondition管道通信volatile
线程安全级别高(内置锁)高(显式锁)中(缓冲区)低(仅可见性)
数据传输能力通过共享对象通过共享对象流式传输单个变量
适用场景线程间协作复杂线程间协作数据流传输状态标志
实现复杂度简单中等中等简单
控制精度一般不适用不适用
阻塞特性阻塞阻塞阻塞非阻塞
锁机制synchronizedReentrantLock内部同步无锁
通信方向多向多向单向多向
通知精确性不精确精确不适用不适用
适用数据类型任意对象任意对象支持连续的二进制数据或文本数据,适合流式处理(如日志、文件内容),不适合离散的对象传输基本类型/对象引用

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

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

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/notifyObject.wait()
Object.notify()
Object.notifyAll()
简单同步
生产者-消费者
必须在 synchronized 中使用
使用 while 循环检查条件
防止虚假唤醒
Conditionlock.newCondition()
condition.await()
condition.signal()
复杂多条件
精确通知
必须与 Lock 配合使用
需手动加解锁
使用 try/finally 保证锁释放
同样需防范虚假唤醒
管道通信PipedInputStream
PipedOutputStream
PipedReader
PipedWriter
数据传输
流处理
需要 connect 连接
单向通信
注意关闭资源
一写一读模式
volatilevolatile 关键字状态标志
一写多读
不保证原子性
适合简单状态同步

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. 如何处理虚假唤醒问题?

    // 正确做法:使用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 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~


异常君
1 声望1 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!