(并发编程)4.Java线程通信:共享内存与消息传递详解
文章目录
这是一个并发编程中最核心的问题之一。
线程通信就像是让多个工人(线程)在同一个工地(进程)上协同工作,他们需要知道彼此的工作进度和状态,以避免混乱(数据竞争)和高效完成任务。
线程通信的核心目标是安全地共享数据和协调执行顺序。
Java 中线程间通信的主要方式可以归为两大类:共享内存和消息传递。我会用多种比喻和实战代码带你彻底理解它们。
第一大类:共享内存 (Shared Memory)
这是最常用的一种方式。多个线程共享同一块内存区域(如共享变量),通过对这个共享区域的读写来进行通信。但关键在于,所有对共享内存的访问必须是同步的、线程安全的,否则会导致数据不一致问题。
这就像几个工人在同一个黑板上写写画画。如果没有规则,大家同时写,黑板上的内容就会一团糟。因此,我们需要建立规则(同步机制)。
方式 1:synchronized + wait()/notify() (内置锁通信)
这是最基础的线程间协作方式。
故事时间:厨师与服务员
想象一个餐厅的窗口:
- 厨师 (生产者线程) 做好菜后,把菜放在窗口。
- 服务员 (消费者线程) 从窗口取菜送给顾客。
- 窗口只能放一盘菜(缓冲区大小为1)。
工作流程:
- 如果窗口是空的,厨师可以放菜,然后通知 (
notify()) 服务员来取。 - 如果窗口是满的,厨师必须等待 (
wait()),直到服务员取走菜后通知他。 - 如果窗口是空的,服务员必须等待 (
wait()),直到厨师做好菜后通知他。 - 如果窗口是满的,服务员可以取菜,然后通知 (
notify()) 厨师可以做下一盘。
代码实战:
public class Restaurant {
// 共享的“菜”
private String dish;
// 标志位:窗口是否有菜可取
private boolean empty = true;
// 厨师线程调用的方法
public synchronized void produce(String newDish) throws InterruptedException {
while (!empty) { // 1. 如果窗口不空(有菜),厨师等待
wait();
}
dish = newDish; // 2. 做菜(修改共享数据)
empty = false; // 3. 标记为不空
System.out.println("厨师做好了: " + dish);
notifyAll(); // 4. 通知所有等待的服务员:“菜好了!”
}
// 服务员线程调用的方法
public synchronized String consume() throws InterruptedException {
while (empty) { // 1. 如果窗口空(没菜),服务员等待
wait();
}
String consumedDish = dish; // 2. 取菜(读取共享数据)
empty = true; // 3. 标记为空
System.out.println("服务员取走了: " + consumedDish);
notifyAll(); // 4. 通知所有等待的厨师:“窗口空了!”
return consumedDish;
}
public static void main(String[] args) {
Restaurant restaurant = new Restaurant();
// 厨师线程
Thread chef = new Thread(() -> {
try {
restaurant.produce("鱼香肉丝");
Thread.sleep(1000);
restaurant.produce("宫保鸡丁");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 服务员线程
Thread waiter = new Thread(() -> {
try {
String food = restaurant.consume();
Thread.sleep(1500);
food = restaurant.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
chef.start();
waiter.start();
}
}
输出:
厨师做好了: 鱼香肉丝
服务员取走了: 鱼香肉丝
厨师做好了: 宫保鸡丁
服务员取走了: 宫保鸡丁
关键点:
wait():释放锁,并让当前线程进入等待状态。notify()/notifyAll():唤醒一个或所有在此对象监视器上等待的线程。- 判断条件 (
while (!empty)) 必须使用while循环,而不是if语句!这是为了防止虚假唤醒(Spurious Wakeup)——线程可能在没有被通知、中断或超时的情况下醒来。
方式 2:Lock + Condition (显式锁通信)
这是 synchronized 方式的增强版,功能更强大、更灵活。
故事时间:更智能的餐厅
现在餐厅升级了,有两个窗口:一个专门出主食,一个专门出饮料。我们用两个 Condition 对象来分别管理等待主食的厨师和服务员、等待饮料的厨师和服务员,通知更精准,效率更高。
代码实战:
import java.util.concurrent.locks.*;
public class ImprovedRestaurant {
private String mainDish;
private String drink;
private boolean mainDishEmpty = true;
private boolean drinkEmpty = true;
private final Lock lock = new ReentrantLock();
private final Condition mainDishCondition = lock.newCondition(); // 主食条件队列
private final Condition drinkCondition = lock.newCondition(); // 饮料条件队列
public void produceMainDish(String dish) throws InterruptedException {
lock.lock();
try {
while (!mainDishEmpty) {
mainDishCondition.await(); // 在“主食条件”上等待
}
mainDish = dish;
mainDishEmpty = false;
System.out.println("厨师做好了主食: " + dish);
mainDishCondition.signal(); // 唤醒一个在“主食条件”上等待的(服务员)线程
} finally {
lock.unlock();
}
}
public String consumeMainDish() throws InterruptedException {
lock.lock();
try {
while (mainDishEmpty) {
mainDishCondition.await(); // 在“主食条件”上等待
}
String dish = mainDish;
mainDishEmpty = true;
System.out.println("服务员取走了主食: " + dish);
mainDishCondition.signal(); // 唤醒一个在“主食条件”上等待的(厨师)线程
return dish;
} finally {
lock.unlock();
}
}
// ... 类似地实现 produceDrink 和 consumeDrink 方法
}
优势:
- 多个等待集:一个锁可以关联多个
Condition,可以对等待的线程进行更精细的分类和管理(如分开生产者和消费者),避免用notifyAll()唤醒所有线程带来的性能开销。 - 更灵活:支持公平锁、可中断的等待、限时等待等。
方式 3:volatile 关键字
volatile 是一种轻量级的同步机制。它只保证变量的可见性和有序性,但不保证原子性。
- 可见性 (Visibility):当一个线程修改了
volatile变量,新值会立即被刷新到主内存中。当其他线程读取该变量时,会从主内存重新加载最新值,而不是使用自己工作内存中的缓存副本。 - 有序性 (Ordering):禁止指令重排序优化。
适用场景: 作为一个标志位 (flag),控制线程的执行和终止。
代码实战:
public class VolatileDemo {
// 使用 volatile 修饰标志位
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) { // 每次循环都会从主内存读取 running 的最新值
// 工作...
}
System.out.println("线程安全地结束了");
}).start();
}
public void stop() {
running = false; // 主线程修改 running,新值立即对其他线程可见
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
demo.start();
Thread.sleep(1000);
demo.stop(); // 1秒后停止线程
}
}
注意: volatile 不能用于需要原子性的操作,例如 count++(读-改-写三步操作)。对于复合操作,仍需使用 synchronized 或 Atomic 类。
第二大类:消息传递 (Message Passing)
这种方式下,线程之间不直接共享数据,而是通过发送消息到某个中间载体来进行通信。生产者线程和消费者线程甚至可以运行在不同的机器上。
方式 4:阻塞队列 (BlockingQueue)
这是 “生产者-消费者”模式的最优解,也是实践中最常用、最推荐的线程通信方式。它封装了所有底层的等待、通知和同步细节,你只需要 put()(放)和 take()(取)就行了。
BlockingQueue 是一个接口,最常用的实现是 ArrayBlockingQueue(有界)和 LinkedBlockingQueue(可选有界/无界)。
故事时间:传送带
一个高效的工厂有一条传送带(阻塞队列)。
- 生产者只需把产品放到传送带末端 (
put)。- 如果传送带满了,生产者会自动停下等待,直到传送带有空位。
- 消费者只需从传送带头部取下产品 (
take)。- 如果传送带空了,消费者会自动停下等待,直到传送带上有产品。
代码实战:
import java.util.concurrent.*;
public class BlockingQueueDemo {
public static void main(String[] args) {
// 创建一个容量为3的阻塞队列(传送带)
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 生产者线程
Thread producer = new Thread(() -> {
try {
String[] dishes = {"菜A", "汤B", "饭C", "甜品D"};
for (String dish : dishes) {
queue.put(dish); // 如果队列满,则阻塞等待
System.out.println("生产了: " + dish + " (队列大小: " + queue.size() + ")");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程(吃得慢)
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 4; i++) {
String dish = queue.take(); // 如果队列空,则阻塞等待
System.out.println("--> 消费了: " + dish + " (队列大小: " + queue.size() + ")");
Thread.sleep(1000); // 消费者比生产者慢
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
输出(注意队列大小的变化):
生产了: 菜A (队列大小: 1)
--> 消费了: 菜A (队列大小: 0)
生产了: 汤B (队列大小: 1)
生产了: 饭C (队列大小: 2)
--> 消费了: 汤B (队列大小: 1)
生产了: 甜品D (队列大小: 2)
--> 消费了: 饭C (队列大小: 1)
--> 消费了: 甜品D (队列大小: 0)
这种方式完美地协调了生产者和消费者不同速度的问题,代码简洁且极其可靠。
总结与对比
| 通信机制 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
synchronized + wait/notify | 共享内存 + 内置锁 | Java原生支持,简单 | 功能单一,不够灵活,易出错 | 简单的线程协作 |
Lock + Condition | 共享内存 + 显式锁 | 功能强大、灵活、精准通知 | 需手动释放锁,代码稍复杂 | 复杂的同步控制(如多个条件) |
volatile | 共享内存(轻量同步) | 性能好,无锁竞争 | 只能保证可见性,不能保证原子性 | 作为状态标志位 |
BlockingQueue | 消息传递 | 极致简单、安全、解耦 | - | 生产者-消费者模式的首选 |
给你的最终建议:
- 首选
BlockingQueue:只要是生产者和消费者模式的场景,无脑用它就对了,这是最佳实践。 - 需要复杂控制时用
Lock+Condition:比如你需要区分不同的等待条件,或者需要公平性、可中断等高级功能。 - 简单的互斥用
synchronized:如果只是单纯地让一段代码不被多个线程同时执行,用它足够简单。 - 标志位用
volatile:记住它只能做标志位,count++这种操作得用AtomicInteger。
通过理解这些不同的通信方式及其背后的原理,你就能在编写多线程程序时做出最合适的选择,写出既正确又高效的代码。
更多推荐


所有评论(0)