Featured image of post Java工程师 并发编程 JMM多线程内存模型

Java工程师 并发编程 JMM多线程内存模型

🌏Java编程思想 并发编程 JMM内存模型 🎯 主要呈现的内容是对JMM(Java多线程内存模型)

🎄多核并发缓存架构

🍭说明

单核CPU不支持真正的并发编程。并发指的是同时执行多个任务或操作的能力,而单核CPU只能在一个时刻执行一个指令。

虽然在操作系统级别可以使用时间片轮转等调度算法将任务分时执行,给用户一种同时运行的感觉,但实际上每个任务都是交替执行的,而非真正同时执行。因此,在单核CPU上进行的并发编程实际上是通过时分复用的方式模拟出来的。 要实现真正的并发编程,需要多核CPU或者多台计算机来同时执行多个线程或进程,以确保任务真正地并行执行。

🎄JMM多线程内存模型

🍭概述

  • 每一个线程都有自己的工作内存 工作内存中存储的是主内存的共享变量的副本

  • 每一个线程直接操作的数据来自于副本数据 而副本数据和主内存的数据的同步由JMM来控制

  • JMM(Java内存模型)并不仅仅针对多核计算机,它是一种规范,用于描述在多线程环境下,共享变量的可见性、有序性和原子性等行为。JMM确保了在多线程编程中,对共享变量的读写操作在不同线程之间具有一定的可见性,防止数据竞争和不确定的行为发生。尽管在多核计算机上使用多线程能更好地发挥硬件性能,但并不意味着单核CPU上不能使用多线程和JMM。即使在单核CPU上,当一个线程在运行时被阻塞,CPU资源可以切换到另一个线程上执行。JMM提供了一套规范和原则,确保在这种情况下,不同线程之间的操作能够按照预期顺序进行。

🍭验证多线程的内存模型

接下来写一个程序来验证多线程的内存模型 在这个程序中 我们期望的结果是在线程t2修改了flag变量为true之后 线程t1能够检测到并执行结束

/**
@author Liu Xianmeng
@createTime 2023/10/6 10:52
@instruction 验证JMM内存模型
*/
public class JMMVerification {
    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
        // 创建线程 1 -> 持续监测flag变量 一旦为true 线程则结束
        Thread t1 = new Thread(new Task_1());
        t1.start(); // 启动线程 1
        // 线程1 睡1秒 等待线程2执行完毕 保证flag变量被修改
        t1.sleep(1000);

        // 创建线程 2 -> 负责修改flag变量为true
        Thread t2 = new Thread(new Task_2());
        t2.start();
    }
}
// 任务 1 -> 持续监测flag变量 一旦为true 线程则结束
class Task_1 implements Runnable {
    @Override
    public void run() {
        // 当flag变量为false的时候持续循环
        while(!JMMVerification.flag) {
            // 持续循环
        }
        // 当flag变量被线程2修改为false的时候 while循环结束
        System.out.println("C Thread_1 M run() -> 检测到flag为true 线程结束");
    }
}
// 任务 2 -> 负责修改flag变量为true
class Task_2 implements Runnable {
    @Override
    public void run() {
        System.out.println("C Thread_2 M run() -> 开始修改flag变量...");
        JMMVerification.flag = true; // 修改flag变量为true
        System.out.println("C Thread_2 M run() -> 修改flag变量为true 执行完成");
    }
}

执行测试 发现线程1始终未检测到flag变量的变化 这也就证明了线程2操作的flag变量是自己的工作内存的flag变量的副本 这个副本被修改后并没有同步线程1的工作内存的flag变量的副本 也就是说线程1的flag副本变量一直没有变 所以线程1一直不会结束

🍭volatile关键字

上面的例子出现的问题如何解决?java中提供了一个关键字volatile 它可以保证同一个变量在不同线程中的可见性 拿上面的例子来说 就是能保证在线程2修改自己工作内存的flag变量的副本后 这个修改能被同步到主内存和其他线程的工作区间 这样的话线程1就能检测到flag被修改了 下面我们来进行验证

给flag变量加上volatile变量进行修饰:

public static volatile boolean flag = false;

重新执行测试 我们可以看到线程1检测到了flag被修改为true 并打印日志 执行结束:

❓那volatile关键字是如何保证变量在多线程环境下的可见性的呢?

(1)当有线程修改一个共享变量的时候 JMM会将当前处理器缓存行的数据写回到系统主内存(2)这个写回操作会使得在其他CPU里缓存了该内存地址的数据无效 从而造成其他CPU从缓存读取该数据的时候失效 强制执行其他CPU的缓存行的重新填充(3)进而运行在其他CPU上的线程的工作区间的变量就被更新了

⚡另外 还可以通过同步机制来保证变量的可见性 例如使用synchronized将flag变量的修改过程进行同步 从而强制flag变量更新后的刷新

🍭JMM缓存不一致的问题

这一小节来看JMM缓存不一致的问题 其实就是在上面的第一个代码示例线程2修改flag变量后 不加volatile关键字时 线程2识别不到flag被修改的问题 在图中演示中会用到JMM的一些原子操作

JMM支持以下数据的原子操作

如何更加清晰地体现这些原子操作呢?红色字体就是上面的原子操作 看下面的图 这个图演示的是没有加volatile的情况

这就是JMM缓存不一致的问题 而通过对变量加volatile关键字的修饰 使得任何一个工作区间的共享变量的修改都能即时更新到主内存并且从主内存刷新到所有的CPU的工作内存 这就实现了多线程环境下变量的可见性