线程间的通信

一、线程间通信核心概念

线程间通信是多线程编程的核心难点之一,其本质是协调多个线程的执行顺序,实现数据共享和逻辑同步。Java 中实现线程通信的核心机制是等待 - 唤醒模型,主要通过以下两种方式实现:

  1. synchronized + Object.wait()/notify()/notifyAll():基于原生同步锁的通信方式
  2. Lock + Condition.await()/signal()/signalAll():基于 JUC 的更灵活的通信方式

二、基础案例:线程交替操作变量

2.1 需求说明

两个线程操作一个初始值为 0 的变量,一个线程对变量 + 1,一个线程对变量 - 1,交替执行 10 轮。

2.2 初始实现(存在隐患)

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

    /**
     * 变量+1操作
     */
    public synchronized void increment() throws InterruptedException {
        // 判断:不满足条件则等待
        if (number != 0) {
            this.wait();
        }
        // 干活:执行核心逻辑
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);
        // 通知:唤醒其他等待线程
        this.notifyAll();
    }

    /**
     * 变量-1操作
     */
    public synchronized void decrement() throws InterruptedException {
        // 判断
        if (number != 1) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);
        // 通知
        this.notifyAll();
    }
}

public class NotifyWaitDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();

        // +1线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    shareData.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();

        // -1线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    shareData.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();
    }
}

2.3 虚假唤醒问题

当线程数量扩展到 4 个(2 个 + 1 线程,2 个 - 1 线程)时,上述代码会出现虚假唤醒问题,导致变量值错乱(如出现 2、3 甚至负数)。

问题根源

  • wait () 方法被唤醒后,会从等待处继续执行,而不是重新判断条件
  • if 判断只会执行一次,无法处理多次唤醒的场景

解决方案:使用 while 代替 if

复制代码
// 修正后的判断逻辑
while (number != 0) {
    this.wait();
}

while (number != 1) {
    this.wait();
}

2.4 使用 Condition 实现通信

对标synchronized:

synchronized是原生的类,jdk当中本身就自带的。所以可以用wait和notify来进行等待和唤醒。但是lock是juc当中的,有自己的一套等待和唤醒的方法。

Condition 是 Lock 锁的配套通信工具,相比 Object 的 wait/notify,提供了更灵活的线程控制能力:

复制代码
class ShareData {
    private Integer number = 0;
    // 创建可重入锁
    private final Lock lock = new ReentrantLock();
    // 创建Condition对象
    private final Condition condition = lock.newCondition();

    /**
     * 变量+1操作
     */
    public void increment() throws InterruptedException {
        lock.lock(); // 加锁
        try {
            // 判断:防止虚假唤醒
            while (number != 0) {
                condition.await(); // 替代wait()
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + ": " + number);
            // 通知
            condition.signalAll(); // 替代notifyAll()
        } finally {
            lock.unlock(); // 解锁
        }
    }

    /**
     * 变量-1操作
     */
    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();
        }
    }
}

三、进阶实战:定制化线程通信

3.1 需求说明

实现三个线程按顺序执行:

  • AA 线程打印 5 次
  • BB 线程打印 10 次
  • CC 线程打印 15 次
  • 循环执行 10 轮

3.2 实现思路

  1. 使用 ReentrantLock 保证线程安全
  2. 为每个线程创建独立的 Condition 对象
  3. 通过标志位控制线程执行顺序
  4. 执行完成后修改标志位并唤醒下一个线程

3.3 完整代码

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

public class ThreadOrderAccess {
    // 全局锁对象
    private final Lock lock = new ReentrantLock();
    
    // 为每个线程创建独立的Condition
    private final Condition conditionA = lock.newCondition();
    private final Condition conditionB = lock.newCondition();
    private final Condition conditionC = lock.newCondition();
    
    // 执行标志位:1-A执行 2-B执行 3-C执行
    private int flag = 1;

    /**
     * A线程打印5次
     */
    public void print5() {
        lock.lock();
        try {
            // 判断:不是A线程执行时机则等待
            while (flag != 1) {
                conditionA.await();
            }
            
            // 干活:打印5次
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "," + i);
            }
            
            // 修改标志位,唤醒B线程
            flag = 2;
            conditionB.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * B线程打印10次
     */
    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.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * C线程打印15次
     */
    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);
            }
            
            // 重置标志位,唤醒A线程开始下一轮
            flag = 1;
            conditionA.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ThreadOrderAccess access = new ThreadOrderAccess();
        
        // 启动A线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                access.print5();
            }
        }, "AA").start();
        
        // 启动B线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                access.print10();
            }
        }, "BB").start();
        
        // 启动C线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                access.print15();
            }
        }, "CC").start();
    }
}

四、面试实战:12A34B...5152Z

4.1 需求分析

  • 线程 1:打印 1-52 的数字
  • 线程 2:打印 A-Z 的字母
  • 执行顺序:12A34B...5152Z

4.2 实现代码

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

public class NumberLetterPrint {
    private final Lock lock = new ReentrantLock();
    private final Condition numCondition = lock.newCondition();
    private final Condition letterCondition = lock.newCondition();
    
    // 控制标志位:true-数字线程执行 false-字母线程执行
    private boolean flag = true;
    // 数字计数器
    private int num = 1;
    // 字母计数器
    private char letter = 'A';

    /**
     * 打印数字的方法
     */
    public void printNumber() {
        lock.lock();
        try {
            while (num <= 52) {
                // 不是数字执行时机则等待
                while (!flag) {
                    numCondition.await();
                }
                
                // 打印两个数字
                System.out.print(num);
                System.out.print(num + 1);
                num += 2;
                
                // 切换标志位,唤醒字母线程
                flag = false;
                letterCondition.signal();
                
                // 数字打印完直接退出,避免等待
                if (num > 52) {
                    break;
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 打印字母的方法
     */
    public void printLetter() {
        lock.lock();
        try {
            while (letter <= 'Z') {
                // 不是字母执行时机则等待
                while (flag) {
                    letterCondition.await();
                }
                
                // 打印一个字母
                System.out.print(letter);
                letter++;
                
                // 切换标志位,唤醒数字线程
                flag = true;
                numCondition.signal();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        NumberLetterPrint print = new NumberLetterPrint();
        
        // 数字线程
        new Thread(print::printNumber, "数字线程").start();
        
        // 字母线程
        new Thread(print::printLetter, "字母线程").start();
    }
}

五、核心总结

5.1 多线程编程模板

  1. 线程操作资源类:将共享数据和操作封装到独立的资源类中,高内聚低耦合
  2. 判断 - 干活 - 通知:在资源类方法中遵循 "判断条件→执行逻辑→通知其他线程" 的流程
  3. 防止虚假唤醒:使用 while 循环替代 if 判断等待条件

5.2 关键注意事项

  1. wait/await 必须在同步块中使用:否则会抛出 IllegalMonitorStateException
  2. 虚假唤醒:多线程场景下必须使用 while 循环重新检查条件
  3. Condition 优势:一个 Lock 可以创建多个 Condition,实现精准的线程唤醒
  4. 锁的释放:finally 块中必须释放锁,避免死锁

5.3 技术选型建议

  • 简单场景:使用 synchronized + wait/notify
  • 复杂场景(需要精准唤醒):使用 Lock + Condition
  • 高并发场景:考虑使用 CountDownLatch、CyclicBarrier 等工具类

通过以上案例和分析,我们可以看到线程间通信的核心是通过等待 - 唤醒机制协调线程执行顺序,结合标志位控制可以实现复杂的线程调度需求。在实际开发中,合理选择通信方式并遵循编程模板,能够有效避免多线程问题。

相关推荐
2401_879693872 小时前
使用Python进行图像识别:CNN卷积神经网络实战
jvm·数据库·python
yunyun321232 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
GIOTTO情2 小时前
Infoseek危机公关全链路技术解析:基于近期热点舆情的落地实践
java
我是人✓2 小时前
从零入门 Servlet:JavaWeb 核心组件的实操与理解
java·servlet
lay_liu2 小时前
Spring Boot 自动配置
java·spring boot·后端
亓才孓2 小时前
JVM讲解
jvm
殷紫川2 小时前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·架构·监控
前端小雪的博客.3 小时前
Java的面向对象:封装详解(0基础入门版)
java·java入门·java面向对象·封装详解·java封装·0基础学java·getter和setter
左左右右左右摇晃3 小时前
Java并发——死锁
java·开发语言·spring