Java并发——线程间的通信

在多线程编程中,线程间通信是一个核心话题。当多个线程需要协同完成某个任务时,它们必须能够互相通知状态的变化,以避免竞态条件和无效的资源占用。Java提供了多种线程间通信的方式,从最基础的 wait/notify 机制,到 Lock 配合 Condition 的灵活方案。本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作。

一、线程间通信的必要性

思考一个简单的场景:两个线程操作一个共享变量,一个线程负责加1,另一个线程负责减1,要求交替执行10轮。如果没有通信机制,线程A可能连续加多次,线程B才减一次,导致结果混乱。线程间通信正是为了解决这类问题------让线程在合适的时机暂停和唤醒,从而保证操作的顺序性和数据的一致性。

二、传统的wait/notify机制

2.1 基本使用

Java中每个对象都有一组监视器方法:wait()notify()notifyAll()。它们必须在同步块(synchronized)中使用,因为需要获取对象的监视器锁。

下面是一个经典的"生产者-消费者"示例,两个线程交替对变量进行+1和-1操作:

java 复制代码
class ShareData {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        // 1. 判断
        if (number != 0) {
            this.wait();
        }
        // 2. 干活
        number++;
        System.out.println(Thread.currentThread().getName() + " => " + number);
        // 3. 通知
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (number != 1) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + " => " + number);
        this.notifyAll();
    }
}

public class WaitNotifyDemo {
    public static void main(String[] args) {
        ShareData data = new ShareData();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }, "B").start();
    }
}

运行结果会交替输出 A => 1B => 0,共10轮。这里的关键点在于:

  • 线程在执行操作前,先判断条件是否满足(number是否为0或1)。

  • 不满足则调用 wait() 进入等待状态,同时释放锁

  • 操作完成后,调用 notifyAll() 唤醒所有等待的线程。

2.2 虚假唤醒问题

当我们将线程数增加到4个(两个加线程,两个减线程),并运行多次后,可能会看到 23 等异常值,甚至出现负数。这是因为 if 判断导致的虚假唤醒

虚假唤醒 指的是线程被唤醒后,条件可能已经不再满足,但程序仍然继续执行。例如,当 number 为0时,A1和A2都等待在 increment 方法中;当B执行减1后调用 notifyAll(),A1和A2同时被唤醒,它们都从 wait() 后继续执行,导致 number 被连续加了两次,变为2。

解决方案 :将 if 改为 while,使线程被唤醒后重新检查条件。这是JDK文档明确要求的。

java 复制代码
public synchronized void increment() throws InterruptedException {
    while (number != 0) {  // 使用while
        this.wait();
    }
    number++;
    System.out.println(Thread.currentThread().getName() + " => " + number);
    this.notifyAll();
}

2.3 wait/notify的局限性

  • 无法精确唤醒notifyAll() 会唤醒所有等待线程,增加了不必要的上下文切换;notify() 只唤醒一个,但无法指定唤醒哪一个。

  • 必须与synchronized绑定 :只能配合 synchronized 使用,不够灵活。

  • 无法响应中断wait() 会抛出 InterruptedException,但线程无法在等待期间主动中断。

三、Lock + Condition:更灵活的通信方式

从JDK 1.5开始,java.util.concurrent.locks 包提供了 Lock 接口和 Condition 接口,弥补了 wait/notify 的不足。

3.1 Condition的基本用法

每个 Condition 对象都相当于一个"队列",通过 await()signal()/signalAll() 实现线程的等待与唤醒。与 wait/notify 类似,使用前必须先获取对应的锁。

将上面的例子用 ReentrantLockCondition 改写:

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ShareData {
    private int number = 0;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + " => " + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

相比 synchronizedLock 提供了更多控制能力(如 tryLock、可中断锁等),而 Condition 则可以创建多个等待队列,实现精确唤醒

3.2 多个Condition实现精准通信

需求:三个线程 A、B、C 依次执行,A 打印5次,B 打印10次,C 打印15次,循环10轮。

这种场景下,需要在线程A执行完后精确唤醒B,B执行完后精确唤醒C,C执行完后精确唤醒A。通过为每个线程创建一个 Condition 对象,并结合一个状态标识,即可轻松实现。

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ShareResource {
    private int flag = 1; // 1: A, 2: B, 3: C
    private final Lock lock = new ReentrantLock();
    private final Condition conditionA = lock.newCondition();
    private final Condition conditionB = lock.newCondition();
    private final Condition conditionC = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            while (flag != 1) {
                conditionA.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 2;
            conditionB.signal(); // 唤醒B
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() {
        lock.lock();
        try {
            while (flag != 2) {
                conditionB.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 3;
            conditionC.signal(); // 唤醒C
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            while (flag != 3) {
                conditionC.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
            flag = 1;
            conditionA.signal(); // 唤醒A
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ConditionDemo {
    public static void main(String[] args) {
        ShareResource resource = new ShareResource();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print5();
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print10();
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) resource.print15();
        }, "C").start();
    }
}

这样,每个线程只会在属于自己的标识位被设置时才执行,执行完后精确唤醒下一个线程,避免了无效的唤醒竞争。

四、经典面试题:交替打印数字和字母

题目:两个线程,一个打印1~52的数字,另一个打印A~Z的字母,要求打印结果为12A34B...5152Z。

分析 :数字线程每次打印两个数字,字母线程每次打印一个字母。可以通过一个标志位来控制切换,也可以用 Condition 来实现精确交替。

4.1 使用 wait/notify 实现

java 复制代码
class Printer {
    private int num = 1;
    private char letter = 'A';
    private boolean printNum = true;

    public synchronized void printNumber() {
        for (int i = 0; i < 26; i++) {
            while (!printNum) {
                try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            System.out.print(num++);
            System.out.print(num++);
            printNum = false;
            notifyAll();
        }
    }

    public synchronized void printLetter() {
        for (int i = 0; i < 26; i++) {
            while (printNum) {
                try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            System.out.print(letter++);
            printNum = true;
            notifyAll();
        }
    }
}

public class PrintDemo {
    public static void main(String[] args) {
        Printer printer = new Printer();
        new Thread(printer::printNumber).start();
        new Thread(printer::printLetter).start();
    }
}

4.2 使用 Condition 实现

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Printer {
    private int num = 1;
    private char letter = 'A';
    private boolean printNum = true;
    private final Lock lock = new ReentrantLock();
    private final Condition numberCondition = lock.newCondition();
    private final Condition letterCondition = lock.newCondition();

    public void printNumber() {
        lock.lock();
        try {
            for (int i = 0; i < 26; i++) {
                while (!printNum) {
                    numberCondition.await();
                }
                System.out.print(num++);
                System.out.print(num++);
                printNum = false;
                letterCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printLetter() {
        lock.lock();
        try {
            for (int i = 0; i < 26; i++) {
                while (printNum) {
                    letterCondition.await();
                }
                System.out.print(letter++);
                printNum = true;
                numberCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

五、总结与最佳实践

  1. 优先使用 Lock + Condition

    如果需要精确控制线程唤醒顺序、支持中断或超时,或者需要更灵活的锁机制,推荐使用 ReentrantLockCondition

  2. 避免虚假唤醒

    无论使用 wait/notify 还是 Condition.await(),判断条件时必须使用 while 循环,而不是 if

  3. finally 中释放锁
    Lock.unlock() 必须放在 finally 块中,确保锁在任何情况下都能被释放,避免死锁。

  4. 使用多个 Condition 实现精确通信

    当需要多个线程协作时,为每个线程创建独立的 Condition,结合状态标志,可以显著提高代码的可读性和效率。

  5. 注意 notify() vs notifyAll()

    使用 Condition.signal() 可以精确唤醒一个等待线程,而 signalAll() 会唤醒所有等待该条件的线程。一般情况下,精确唤醒能减少不必要的上下文切换。

相关推荐
knowwen2 分钟前
2026年4月13日 Java笔试题
java·开发语言
环流_4 分钟前
多线程2(Java线程的状态)
java·开发语言
_oP_i6 分钟前
python 之playwright 介绍
开发语言·python
zihao_tom8 分钟前
Spring 简介
java·后端·spring
浪客川12 分钟前
【百例RUST - 009】容器 Vector
开发语言·rpc·rust
C雨后彩虹24 分钟前
Java Lambda & Stream 避坑指南:20个高频错误案例分析与修复
java·stream·lambda·并行流
环流_27 分钟前
多线程3(线程安全问题及解决方案)
java·开发语言
覆东流43 分钟前
第2天:Python变量与数据类型
开发语言·后端·python
FeBaby1 小时前
Java 高并发场景下 Redis 分布式锁(UUID+Lua)最佳实践
java·redis·分布式
Gofarlic_oms11 小时前
制定企业Citrix虚拟化软件资产管理政策框架
运维·服务器·开发语言·matlab·负载均衡