你想了解 Java 中线程间通信的具体方式,我会从基础到进阶梳理 6 种核心通信方式,结合代码示例和适用场景,让你清楚每种方式的原理、用法和优缺点,同时区分"通信"(线程间交换数据/协调行为)和"同步"(线程间互斥)的核心差异。
核心结论先明确
Java 线程间通信的本质是线程间共享数据/协调执行顺序,核心方式分为两类:
- 基础方式 :基于
Object类的wait()/notify()/notifyAll()(最原生)、join()、sleep()(间接协调); - 进阶方式 :基于
Lock锁的Condition、BlockingQueue(生产消费模型)、CountDownLatch/CyclicBarrier(线程同步工具)、ThreadLocal(反向通信:线程内隔离)。
一、方式1:wait() + notify()/notifyAll()(最原生,基于对象监视器)
这是 Java 最基础的线程通信方式,依赖 Object 类的内置方法,核心是通过对象锁实现线程的等待/唤醒。
核心原理
wait():线程释放对象锁,进入等待状态,直到被其他线程唤醒;notify():唤醒等待该对象锁的任意一个线程;notifyAll():唤醒等待该对象锁的所有线程;- 必须在
synchronized代码块/方法中使用(持有对象锁才能调用)。
代码示例(生产消费模型)
java
/**
* 基于wait/notify的生产消费模型
* 生产者生产数据,消费者消费数据,通过wait/notify协调
*/
public class WaitNotifyDemo {
// 共享数据容器
private final Object lock = new Object();
private int data = 0;
private boolean hasData = false; // 标记是否有数据
// 生产者线程:生产数据
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
synchronized (lock) {
// 已有数据,等待消费者消费
while (hasData) {
try {
lock.wait(); // 释放锁,进入等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 生产数据
data = i;
System.out.println("生产者生产:" + data);
hasData = true;
lock.notify(); // 唤醒消费者
}
}
}
}
// 消费者线程:消费数据
class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
synchronized (lock) {
// 无数据,等待生产者生产
while (!hasData) {
try {
lock.wait(); // 释放锁,进入等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 消费数据
System.out.println("消费者消费:" + data);
hasData = false;
lock.notify(); // 唤醒生产者
}
}
}
}
// 测试
public static void main(String[] args) {
WaitNotifyDemo demo = new WaitNotifyDemo();
new Thread(demo.new Producer()).start();
new Thread(demo.new Consumer()).start();
}
}
输出结果(生产消费交替执行)
生产者生产:0
消费者消费:0
生产者生产:1
消费者消费:1
生产者生产:2
消费者消费:2
生产者生产:3
消费者消费:3
生产者生产:4
消费者消费:4
适用场景
- 简单的生产消费模型;
- 线程间的基础等待/唤醒协调;
- 缺点:只能唤醒任意/所有线程,无法精准唤醒指定线程。
二、方式2:Condition(基于Lock锁,精准通信)
Condition 是 java.util.concurrent.locks 包下的接口,替代 wait()/notify(),核心优势是可以创建多个条件队列,精准唤醒指定线程。
核心原理
lock.newCondition():创建一个条件对象;condition.await():替代wait(),释放锁并等待;condition.signal():替代notify(),唤醒该条件队列的一个线程;condition.signalAll():替代notifyAll(),唤醒该条件队列的所有线程;- 必须在
lock.lock()/lock.unlock()之间使用。
代码示例(精准唤醒生产者/消费者)
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 基于Condition的精准通信:分别唤醒生产者/消费者
*/
public class ConditionDemo {
private final Lock lock = new ReentrantLock();
private final Condition producerCond = lock.newCondition(); // 生产者条件
private final Condition consumerCond = lock.newCondition(); // 消费者条件
private int data = 0;
private boolean hasData = false;
// 生产者
public void produce() {
lock.lock();
try {
while (hasData) {
producerCond.await(); // 生产者等待
}
data++;
System.out.println("生产者生产:" + data);
hasData = true;
consumerCond.signal(); // 精准唤醒消费者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
// 消费者
public void consume() {
lock.lock();
try {
while (!hasData) {
consumerCond.await(); // 消费者等待
}
System.out.println("消费者消费:" + data);
hasData = false;
producerCond.signal(); // 精准唤醒生产者
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
// 测试
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
// 生产者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.produce();
}
}).start();
// 消费者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.consume();
}
}).start();
}
}
适用场景
- 需要精准唤醒指定线程的场景(如多生产者多消费者);
- 替代
wait()/notify(),灵活性更高; - 优点:多条件队列,精准通信;缺点:需手动释放锁,易遗漏
unlock()。
三、方式3:BlockingQueue(生产消费模型,最易用)
BlockingQueue(阻塞队列)是 Java 并发包提供的"开箱即用"通信工具,核心是队列满时生产者阻塞,队列空时消费者阻塞,无需手动处理等待/唤醒。
核心原理
- 常用实现:
ArrayBlockingQueue(数组实现,有界)、LinkedBlockingQueue(链表实现,可选有界); put():队列满时阻塞生产者;take():队列空时阻塞消费者;- 内置了
Condition实现,底层仍是lock+await()/signal()。
代码示例(基于阻塞队列的生产消费)
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 基于BlockingQueue的生产消费模型(无需手动处理等待/唤醒)
*/
public class BlockingQueueDemo {
// 有界阻塞队列,容量为2
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);
// 生产者
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
queue.put(i); // 队列满则阻塞
System.out.println("生产者生产:" + i + ",队列大小:" + queue.size());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
int data = queue.take(); // 队列空则阻塞
System.out.println("消费者消费:" + data + ",队列大小:" + queue.size());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// 测试
public static void main(String[] args) {
BlockingQueueDemo demo = new BlockingQueueDemo();
new Thread(demo.new Producer()).start();
new Thread(demo.new Consumer()).start();
}
}
输出结果(队列满时生产者阻塞)
生产者生产:0,队列大小:1
生产者生产:1,队列大小:2
消费者消费:0,队列大小:1
生产者生产:2,队列大小:2
消费者消费:1,队列大小:1
生产者生产:3,队列大小:2
消费者消费:2,队列大小:1
生产者生产:4,队列大小:2
消费者消费:3,队列大小:1
消费者消费:4,队列大小:0
适用场景
- 生产消费模型(最推荐的方式);
- 线程间数据传递(如任务队列);
- 优点:无需手动处理锁和等待/唤醒,代码简洁;缺点:仅适用于队列式通信。
四、方式4:join()(线程等待,简单协调)
join() 是 Thread 类的方法,核心是让当前线程等待目标线程执行完毕后再继续,属于"被动通信"(通过等待实现顺序协调)。
核心原理
t.join():当前线程等待线程t执行完;t.join(long millis):等待millis毫秒后超时继续。
代码示例(主线程等待子线程执行)
java
/**
* 基于join()的线程协调:主线程等待子线程执行完毕
*/
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("子线程执行:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
t1.start();
t1.join(); // 主线程等待t1执行完毕
System.out.println("主线程继续执行:子线程已完成");
}
}
输出结果(主线程等待子线程)
子线程执行:0
子线程执行:1
子线程执行:2
主线程继续执行:子线程已完成
适用场景
- 简单的线程顺序执行(如主线程等待子线程加载数据);
- 优点:使用简单;缺点:仅能实现"等待完成",无法传递数据。
五、方式5:并发工具类(CountDownLatch/CyclicBarrier,批量同步)
这类工具用于多线程批量同步,属于"间接通信",核心是协调多个线程的执行阶段。
1. CountDownLatch(倒计时门闩)
- 原理:初始化一个计数器,线程完成任务后
countDown(),主线程await()等待计数器归0; - 场景:主线程等待多个子线程完成初始化。
2. CyclicBarrier(循环栅栏)
- 原理:多个线程到达栅栏处
await(),等待所有线程到达后一起继续执行; - 场景:多线程分阶段执行任务(如所有线程完成第一阶段后,再一起执行第二阶段)。
代码示例(CountDownLatch)
java
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch:主线程等待3个子线程完成任务
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 计数器=3
for (int i = 0; i < 3; i++) {
int taskId = i;
new Thread(() -> {
System.out.println("子线程" + taskId + "执行任务");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
latch.countDown(); // 计数器-1
}).start();
}
latch.await(); // 主线程等待计数器归0
System.out.println("所有子线程完成任务,主线程继续");
}
}
适用场景
- 多线程初始化、批量任务执行;
- 优点:批量同步简单;缺点:CountDownLatch 计数器不可重置,CyclicBarrier 可循环使用。
六、方式6:ThreadLocal(反向通信:线程内隔离)
ThreadLocal 严格来说是"线程内数据隔离",但也可视为一种特殊的"通信"------线程内部的不同方法间共享数据,避免线程间数据干扰。
核心原理
ThreadLocal<T>:为每个线程创建独立的变量副本;set(T):设置当前线程的变量副本;get():获取当前线程的变量副本;remove():移除当前线程的变量副本(避免内存泄漏)。
代码示例(ThreadLocal 线程内共享数据)
java
/**
* ThreadLocal:线程内共享数据,线程间隔离
*/
public class ThreadLocalDemo {
// 定义ThreadLocal变量,存储线程专属数据
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 线程1:设置并获取自己的副本
new Thread(() -> {
threadLocal.set("线程1的专属数据");
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.remove(); // 用完移除
}, "线程1").start();
// 线程2:设置并获取自己的副本
new Thread(() -> {
threadLocal.set("线程2的专属数据");
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.remove(); // 用完移除
}, "线程2").start();
}
}
输出结果(线程间数据隔离)
线程1:线程1的专属数据
线程2:线程2的专属数据
适用场景
- 线程内共享数据(如用户登录信息、数据库连接);
- 避免多线程共享变量的同步问题;
- 优点:线程安全,无需加锁;缺点:易引发内存泄漏(需手动
remove())。
七、各通信方式对比(快速选型)
| 方式 | 核心优势 | 适用场景 | 缺点 |
|---|---|---|---|
| wait()/notify() | 原生,无需额外依赖 | 简单生产消费 | 无法精准唤醒,需手动处理锁 |
| Condition | 精准唤醒指定线程 | 多生产者多消费者 | 需手动释放锁 |
| BlockingQueue | 开箱即用,无需手动处理同步 | 生产消费模型(推荐) | 仅适用于队列式通信 |
| join() | 简单的线程顺序协调 | 主线程等待子线程完成 | 无法传递数据 |
| CountDownLatch/栅栏 | 批量线程同步 | 多线程初始化、分阶段执行 | CountDownLatch 计数器不可重置 |
| ThreadLocal | 线程内数据隔离,无需加锁 | 线程内共享数据(如用户上下文) | 易内存泄漏,需手动 remove |
总结
- 生产消费模型优先选 BlockingQueue:代码简洁,无需手动处理等待/唤醒,是工业界主流;
- 精准唤醒选 Condition :替代
wait()/notify(),灵活性更高; - 简单顺序协调选 join():适用于主线程等待子线程完成;
- 批量同步选 CountDownLatch/CyclicBarrier:适配多线程分阶段执行;
- 线程内数据共享选 ThreadLocal:避免同步问题,注意内存泄漏。
如果需要某一种方式的进阶用法(如多生产者多消费者的 Condition 实现、BlockingQueue 的性能优化),可以告诉我,我会补充详细示例。