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() 会唤醒所有等待该条件的线程。一般情况下,精确唤醒能减少不必要的上下文切换。

相关推荐
__Yvan1 小时前
Kotlin 的 ?.let{} ?: run{} 真的等价于 if-else 吗?
android·开发语言·前端·kotlin
小小小米粒1 小时前
[特殊字符] 正常部署 AI + 流式输出(Stream)[特殊字符] 为什么会 CPU 炸了?
开发语言·python
烟花巷子1 小时前
C++中的解释器模式
开发语言·c++·算法
用户298698530142 小时前
Java: 从 Word 文档中提取文本和图像
java·后端
暮冬-  Gentle°2 小时前
C++中的策略模式高级应用
开发语言·c++·算法
皙然2 小时前
吃透进程与线程:从概念到实战,破解并发编程核心难题
java·开发语言
2401_879693872 小时前
C++中的代理模式高级应用
开发语言·c++·算法
冬夜戏雪2 小时前
HashMAP底层原理和扰动hash的例子
java·开发语言
咸鱼2.02 小时前
【java入门到放弃】计算机网络
java·开发语言·计算机网络