【JUC】线程间通信

Java 线程间通信,简单说就是多个线程如何协调工作、交换数据、控制执行顺序。因为线程是并发执行的,如果不加控制,很容易出现数据错乱、重复执行、死锁等问题。

下面按"常用方式 → 底层原理 → 典型场景"来讲。


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

多个线程共享进程内存,但每个线程有自己的工作内存(CPU 缓存、寄存器等)。如果一个线程改了共享变量,另一个线程可能看不到,或者看到的是旧值。

典型问题:

  • 生产者先生产,消费者才能消费
  • 一个线程必须等另一个线程完成才能继续
  • 多个线程要互斥访问同一个资源
  • 线程之间要传递结果

这些都需要线程间通信机制。


二、主要通信方式

1. synchronized + wait/notify/notifyAll

这是最经典的 Java 线程通信方式,基于对象监视器(Monitor)。

三个方法都必须在 synchronized 块或方法中调用,否则抛 IllegalMonitorStateException。

  • wait():释放锁并进入等待队列,直到被唤醒
  • notify():随机唤醒一个等待该对象的线程
  • notifyAll():唤醒所有等待该对象的线程

经典生产者-消费者模型:

java 复制代码
class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;

    public synchronized void produce(int value) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // 满了,生产者等待
        }
        queue.offer(value);
        System.out.println("生产: " + value);
        notifyAll(); // 唤醒消费者
    }

    public synchronized int consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 空了,消费者等待
        }
        int value = queue.poll();
        System.out.println("消费: " + value);
        notifyAll(); // 唤醒生产者
        return value;
    }
}

关键点:

  • 用 while 而不是 if 判断条件,防止虚假唤醒
  • 生产者和消费者等待的是同一个锁对象
  • notifyAll() 比 notify() 更安全,避免唤醒错线程导致死等

2. ReentrantLock + Condition

Condition 可以理解为更灵活的 wait/notify,一个锁可以创建多个等待队列。

java 复制代码
class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();
            }
            queue.offer(value);
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            int value = queue.poll();
            notFull.signalAll();
            return value;
        } finally {
            lock.unlock();
        }
    }
}

优点:

  • 可以精确唤醒某一类线程,比如只唤醒生产者或只唤醒消费者
  • 支持公平锁、可中断等待、超时等待
  • 比 synchronized 更灵活

3. volatile + CAS / 原子类

volatile 保证可见性和有序性,但不保证原子性。

适合场景:

  • 一个线程写,多个线程读的状态标志
  • 配合 AtomicInteger、AtomicReference 等原子类做无锁并发
java 复制代码
class Flag {
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // do something
        }
    }
}

注意:volatile 不能替代锁。像 i++ 这种复合操作,即使变量是 volatile,也不是线程安全的。


4. BlockingQueue 阻塞队列

这是实际开发中最推荐的生产者-消费者实现方式,底层已经封装好了等待/唤醒逻辑。

常用实现:

  • ArrayBlockingQueue:有界数组队列
  • LinkedBlockingQueue:链表队列,可选有界
  • SynchronousQueue:不存储元素,直接传递
  • PriorityBlockingQueue:优先级队列
  • DelayQueue:延迟队列
java 复制代码
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);

// 生产者
new Thread(() -> {
    try {
        queue.put(1); // 满了会阻塞
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 消费者
new Thread(() -> {
    try {
        Integer value = queue.take(); // 空了会阻塞
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

优点:

  • 不用手写 wait/notify,减少出错
  • 线程安全,性能较好
  • 支持超时、中断

5. CountDownLatch / CyclicBarrier / Semaphore

这些是并发工具类,用于更复杂的线程协调。

CountDownLatch:一个线程等多个线程完成。

java 复制代码
CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // do something
        latch.countDown();
    }).start();
}

latch.await(); // 主线程等待三个线程完成

CyclicBarrier:多个线程互相等待,到齐后一起继续,可重复使用。

java 复制代码
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到齐");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // do something
        barrier.await();
    }).start();
}

Semaphore:控制同时访问某个资源的线程数量。

java 复制代码
Semaphore semaphore = new Semaphore(2); // 最多两个线程同时访问

semaphore.acquire();
try {
    // 访问资源
} finally {
    semaphore.release();
}

6. CompletableFuture 和 Future

用于线程间传递异步计算结果。

java 复制代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 42;
});

future.thenAccept(result -> System.out.println(result));

适合任务编排、异步回调、多任务合并等场景。


7. Thread.join()

让当前线程等待另一个线程结束。

java 复制代码
Thread t = new Thread(() -> {
    // do something
});
t.start();
t.join(); // 主线程等 t 执行完

底层原理是 wait/notify,当线程结束时 JVM 会调用 notifyAll()。


8. LockSupport.park/unpark

更底层的线程阻塞/唤醒工具,不需要锁,也不要求顺序。

java 复制代码
Thread t = new Thread(() -> {
    LockSupport.park(); // 阻塞
    System.out.println("被唤醒");
});
t.start();

LockSupport.unpark(t); // 唤醒指定线程

特点:

  • unpark 可以先于 park 调用,许可证机制
  • 是 LockSupport 的基础,AQS 内部大量使用

三、底层原理简析

1. 对象监视器(Monitor)

每个 Java 对象都有一个监视器,synchronized 就是基于它实现的。

  • 竞争锁失败的线程进入 EntryList
  • 调用 wait() 的线程进入 WaitSet
  • notify/notifyAll 把 WaitSet 中的线程移到 EntryList 重新竞争锁

2. AQS(AbstractQueuedSynchronizer)

ReentrantLock、Semaphore、CountDownLatch 等都基于 AQS。

核心思想:

  • 一个 volatile int state 表示同步状态
  • 一个 CLH 双向队列存放等待线程
  • 通过 CAS 修改 state
  • 获取失败则入队并 LockSupport.park

3. 内存可见性

  • volatile:写操作立即刷新到主内存,读操作从主内存读取
  • synchronized:释放锁前把共享变量刷回主内存,获取锁时从主内存读取
  • final:正确构造后对其他线程可见

四、如何选择

场景 推荐方式
简单等待/唤醒 synchronized + wait/notify
需要多个等待条件 ReentrantLock + Condition
生产者-消费者 BlockingQueue
一个线程等多个线程 CountDownLatch
多个线程互相等待 CyclicBarrier
控制并发数量 Semaphore
异步结果传递 CompletableFuture
简单状态标志 volatile
底层阻塞唤醒 LockSupport

五、常见坑

  1. wait/notify 不在 synchronized 中调用 → 抛异常
  2. 用 if 判断等待条件 → 虚假唤醒导致错误
  3. notify 唤醒错线程 → 可能死等,优先用 notifyAll
  4. volatile 当锁用 → 复合操作不安全
  5. 忘记 unlock → 用 try/finally
  6. 线程中断被吞掉 → 捕获 InterruptedException 后要么处理,要么重新设置中断标志
  7. 死锁 → 多把锁顺序不一致、锁嵌套等

六、一句话总结

Java 线程间通信的核心是:共享内存 + 同步机制 。简单场景用 synchronized + wait/notify,复杂场景用 Lock + Condition,生产者-消费者优先用 BlockingQueue,线程协调用 CountDownLatch/CyclicBarrier/Semaphore,异步编排用 CompletableFuture。底层都离不开可见性、原子性和有序性这三个问题。

相关推荐
yuniko-n1 小时前
【JUC】关于 Java 线程介绍
java·开发语言
HelloWorld工程师1 小时前
@Primary 注解
java·spring
geovindu1 小时前
rust: Borg Pattern(续)
开发语言·后端·设计模式·rust·博格模式
aqiu1111113 小时前
【C++算法打怪专栏】8. 字符串转换整数 (atoi)
开发语言·c++·算法
爱吃香菜的初学者3 小时前
十四.Linux——消息队列
linux·运维·开发语言
迅猛龙办公室3 小时前
python实现倒序输出数字
开发语言·python
夏幻灵9 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
程序猿编码9 小时前
榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码
开发语言·c++·大模型推理·qwen3·多模态推理
bkspiderx11 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader