Java并发线程间的通信最佳实践

 更新时间:2026年03月25日 08:55:43   作者:左左右右左右摇晃  
这篇文章主要介绍了Java并发线程间的通信最佳实践,本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作,需要的朋友可以参考下

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

一、线程间通信的必要性

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

二、传统的wait/notify机制

2.1 基本使用

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

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

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 => 1 和 B => 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文档明确要求的。

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 类似,使用前必须先获取对应的锁。

将上面的例子用 ReentrantLock 和 Condition 改写:

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 对象,并结合一个状态标识,即可轻松实现。

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 实现

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 实现

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();
        }
    }
}

五、总结与最佳实践

  • 优先使用 Lock + Condition
    • 如果需要精确控制线程唤醒顺序、支持中断或超时,或者需要更灵活的锁机制,推荐使用 ReentrantLock 和 Condition
  • 避免虚假唤醒
    • 无论使用 wait/notify 还是 Condition.await(),判断条件时必须使用 while 循环,而不是 if
  • 在 finally 中释放锁
  • Lock.unlock() 必须放在 finally 块中,确保锁在任何情况下都能被释放,避免死锁。
  • 使用多个 Condition 实现精确通信
    • 当需要多个线程协作时,为每个线程创建独立的 Condition,结合状态标志,可以显著提高代码的可读性和效率。
  • 注意 notify() vs notifyAll()
    • 使用 Condition.signal() 可以精确唤醒一个等待线程,而 signalAll() 会唤醒所有等待该条件的线程。一般情况下,精确唤醒能减少不必要的上下文切换。

到此这篇关于Java并发线程间的通信最佳实践的文章就介绍到这了,更多相关Java线程间的通信内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 关于logback日志级别动态切换的四种方式

    关于logback日志级别动态切换的四种方式

    这篇文章主要介绍了关于logback日志级别动态切换的四种方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • SpringBoot自定义线程池,执行定时任务方式

    SpringBoot自定义线程池,执行定时任务方式

    这篇文章主要介绍了SpringBoot自定义线程池,执行定时任务方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-04-04
  • springboot后端存储富文本内容的思路与步骤(含图片内容)

    springboot后端存储富文本内容的思路与步骤(含图片内容)

    在所有的编辑器中,大概最受欢迎的就是富文本编辑器和MarkDown编辑器了,下面这篇文章主要给大家介绍了关于springboot后端存储富文本内容的思路与步骤的相关资料,需要的朋友可以参考下
    2023-04-04
  • Springboot获取bean实例之SpringContextUtil详解

    Springboot获取bean实例之SpringContextUtil详解

    这篇文章主要介绍了Springboot获取bean实例之SpringContextUtil使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • Java实现猜字小游戏

    Java实现猜字小游戏

    这篇文章给大家分享小编随手写的猜字小游戏,基于java代码写的,感兴趣的朋友跟随小编一起看看吧
    2019-11-11
  • Springboot集成RabbitMQ死信队列的实现

    Springboot集成RabbitMQ死信队列的实现

    在大多数的MQ中间件中,都有死信队列的概念。本文主要介绍了Springboot集成RabbitMQ死信队列的实现,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • 深入解析Java中的编码转换以及编码和解码操作

    深入解析Java中的编码转换以及编码和解码操作

    这篇文章主要介绍了Java中的编码转换以及编码和解码操作,文中详细解读了编码解码的相关IO操作以及内存使用方面的知识,需要的朋友可以参考下
    2016-02-02
  • ThreadLocal内存泄漏常见要点解析

    ThreadLocal内存泄漏常见要点解析

    这篇文章主要介绍了ThreadLocal内存泄漏常见要点,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • Lombok中@Builder和@SuperBuilder注解的用法案例

    Lombok中@Builder和@SuperBuilder注解的用法案例

    @Builder 是 lombok 中的注解,可以使用builder()构造的Person.PersonBuilder对象进行链式调用,给所有属性依次赋值,这篇文章主要介绍了Lombok中@Builder和@SuperBuilder注解的用法,需要的朋友可以参考下
    2023-01-01
  • springboot 如何取消starter的自动注入

    springboot 如何取消starter的自动注入

    这篇文章主要介绍了springboot 如何取消starter的自动注入操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09

最新评论