这是一个并发编程中最核心的问题之一。

线程通信就像是让多个工人(线程)在同一个工地(进程)上协同工作,他们需要知道彼此的工作进度和状态,以避免混乱(数据竞争)和高效完成任务。

线程通信的核心目标是安全地共享数据和协调执行顺序

Java 中线程间通信的主要方式可以归为两大类:共享内存消息传递。我会用多种比喻和实战代码带你彻底理解它们。


第一大类:共享内存 (Shared Memory)

这是最常用的一种方式。多个线程共享同一块内存区域(如共享变量),通过对这个共享区域的读写来进行通信。但关键在于,所有对共享内存的访问必须是同步的、线程安全的,否则会导致数据不一致问题。

这就像几个工人在同一个黑板上写写画画。如果没有规则,大家同时写,黑板上的内容就会一团糟。因此,我们需要建立规则(同步机制)。

方式 1:synchronized + wait()/notify() (内置锁通信)

这是最基础的线程间协作方式。

故事时间:厨师与服务员
想象一个餐厅的窗口:

  • 厨师 (生产者线程) 做好菜后,把菜放在窗口。
  • 服务员 (消费者线程) 从窗口取菜送给顾客。
  • 窗口只能放一盘菜缓冲区大小为1)。

工作流程:

  1. 如果窗口是空的,厨师可以放菜,然后通知 (notify()) 服务员来取。
  2. 如果窗口是满的,厨师必须等待 (wait()),直到服务员取走菜后通知他。
  3. 如果窗口是空的,服务员必须等待 (wait()),直到厨师做好菜后通知他。
  4. 如果窗口是满的,服务员可以取菜,然后通知 (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++(读-改-写三步操作)。对于复合操作,仍需使用 synchronizedAtomic 类。


第二大类:消息传递 (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消息传递极致简单、安全、解耦-生产者-消费者模式的首选

给你的最终建议:

  1. 首选 BlockingQueue:只要是生产者和消费者模式的场景,无脑用它就对了,这是最佳实践。
  2. 需要复杂控制时用 Lock + Condition:比如你需要区分不同的等待条件,或者需要公平性、可中断等高级功能。
  3. 简单的互斥用 synchronized:如果只是单纯地让一段代码不被多个线程同时执行,用它足够简单。
  4. 标志位用 volatile:记住它只能做标志位,count++ 这种操作得用 AtomicInteger

通过理解这些不同的通信方式及其背后的原理,你就能在编写多线程程序时做出最合适的选择,写出既正确又高效的代码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐