你了解的JMM内存模型是它吗?

简介: JMM(java memory model)java内存模型,它并没有实际的体现,它是一个规则,都知道ava是跨平台语言,在个操作系统中内存都有一定的差异性,每个系统的并发不一致,JMM的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时JMM也规范了JVM如何与计算机内存进行交互。JMM就是Java自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的"一次编写,到处运行"

1. JMM做了什么

JMM(java memory model)java内存模型,它并没有实际的体现,它是一个规则,都知道ava是跨平台语言,在个操作系统中内存都有一定的差异性,每个系统的并发不一致,JMM的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时JMM也规范了JVM如何与计算机内存进行交互。JMM就是Java自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的"一次编写,到处运行"

2. JMM抽象图


上面的抽象图可以看到共享变量是在主内存里,但是在修改的时候线程会将变量拷贝到自己的工作空间内,修改后再刷回住内存的这样一个概念,中间线程操作变量是由JMM控制的,下面看一下它的规则。

3. 规则

  • 可见性

上面说了线程是将变量拿到自己的工作空间进行修改后再写回到主内存,如果线程1刚拿到了变量而线程2把变量进行修改了,线程1不知道而他执行的业务里又使用了变量那它执行的结果就出现了问题,下面代码示例:

public static void main(String[] args) {
        // 定义一个内部数据类
        class IsLookData{
            int i = 0;
            // 调用这个方法修改值
            public void add10(){
                this.i = 10;
            }
        }
        IsLookData isLookData = new IsLookData();
        // lambda表达式创建线程
        new Thread(()->{
            // 休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 加值并写会主内存
            isLookData.add10();
            // 输出主内存的值查看是否更改成功
            System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
        }).start();
        // 等待变量值修改再执行下一步
        while (isLookData.i == 0){
            // 循环等待
        }
        System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
    }
复制代码

执行上面的代码会怎么:会死循环,main线程在启动后拿到的值是0所以会进入while循环等待值不为0,线程里sleep是让它的问题放大,就是保证main线程进入while后变量才发生变化,否则有可能不进入循环就直接验证非0结束了,而线程1修改变量后并没有通知main线程,也就是main线程看不到变量发生了改变所以它会一直死循环。

解决办法,在变量加入volatile,在这个变量发生修改的时候会通知使用这个变量的所有线程重新拿取变量

  • 原子性

原子性的概念都清楚不可分割,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

代码示例:

class JmmAtomic{
    public static void main(String[] args) {
        // 定义一个内部数据类
        class IsLookData{
            volatile int i = 0;
            // 调用这个方法修改值
            public void add(){
                this.i++;
            }
        }
        IsLookData isLookData = new IsLookData();
        // 创建50个线程
        for (int i = 0; i < 50; i++) {
            // lambda表达式创建线程
            new Thread(()->{
                // 每个线程执行1000次数字累加
                for (int j = 0; j < 1000; j++) {
                    isLookData.add();
                }
            }).start();
        }
        // 休眠1s
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出结果
        System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
    }
}
复制代码

执行结果:

可以看到我们50个线程每个加1000个应该是50000个,最终的值却不足50000,我们是使用了volatile的,而volatile是不保证原子性的,所以值出现了差异,怎么解决这样的问题,想到的肯定是加锁synchronized、locks,这样肯定可以解决这个问题,但是我只为了一个数值的变化就加锁,锁可是很影响效率的,所以使用原子类,我们用的int所以使用原子类AtomicInteger找到里面等于i++的方法incrementAndGet,现在再执行看一下结果,代码:

public static void main(String[] args) {
        // 定义一个内部数据类
        class IsLookData{
            volatile int i = 0;
            // 原子类
            AtomicInteger atomicInteger = new AtomicInteger();
            // 调用这个方法修改值
            public void add(){
                this.i++;
                // 等同于i++
                atomicInteger.incrementAndGet();
            }
        }
        IsLookData isLookData = new IsLookData();
        // 创建50个线程
        for (int i = 0; i < 50; i++) {
            // lambda表达式创建线程
            new Thread(()->{
                // 每个线程执行1000次数字累加
                for (int j = 0; j < 1000; j++) {
                    isLookData.add();
                }
            }).start();
        }
        // 休眠1s
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出结果
        System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
        System.out.println(Thread.currentThread().getName()+" 线程原子类的值"+isLookData.atomicInteger.get());
    }
复制代码
结果:
复制代码

多输出几次,原子类的值一直是50000,结果正确,原子类的底层使用的是CAS保证的原子性。

  • 有序性

有序性,在我们的机器执行代码的时候他并不是按照我们写的顺序执行的,它会为了优化执行顺序进行指令重排,单线程的情况下是没问题的,保证了单线程的执行,而多线程的情况下因为交替执行很可能因为指令重拍而出现错误。

public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 1 + 5;
        c = c * b;
        int d = c * a;
    }
复制代码

在上面的代码重排后,a可以是第一个执行的,b也可以是第一个执行的,c也可以是第一个执行的,但是5和6行不可能是第一个执行的,因为它有数据依赖性,c = c*b依赖了c和b所以它要等待c和b有了后再执行,它是没问题的,但是第六行会出现问题吗?

会的,指令重拍后第6行去了第5行,我数据依赖了c和a,(c = 6、a = 1)它俩现在都有所以可以执行,但是c的值不对,少了一部c = c * b; 所以重排后的结果是 6 * 1 = 6 正确结果是 6 * 2 * 1 = 12

怎么解决:

使用volatile修饰方法或变量,它会在执行禁止重拍的代码前后加上内存屏障告诉程序你不要重拍这部分的代码以保证顺序执行,而synchronized是怎么保证有序的,synchronized是直接将方法和块锁定变成单线程方式每次只有一个线程执行里面的内容,如果synchronized是加在方法上是没问题的,如果是加在代码块上是可能有问题的:

// 单例模式
class OneDemo{
    private static OneDemo oneDemo = null;
    private OneDemo(){
        System.out.println("构造方法");
    }
    //开放一个公有方法,判断是否已经存在实例,有返回,没有新建一个在返回
    public static OneDemo getInstance(){
        if(oneDemo == null){
            synchronized (OneDemo.class){
                oneDemo = new OneDemo();
            }
        }
        return oneDemo;
    }
}
复制代码

原因在于某一个线程执行到第一次检测,读取到的oneDemo不为null时,而oneDemo的引用对象没有完成初始化。

oneDemo = new OneDemo(); 这行代码可以说是由下面三步完成的

memory = allocate(); // 1.分配对象内存空间
oneDemo(memory); // 2.初始化对象
oneDemo = memory; // 3.设置oneDemo指向刚分配的内存地址,现在的oneDemo!=null
复制代码

而在并发环境下其他线程会到if(oneDemo == null)判断再到锁,而在指令重拍后我们的第三行优先执行,但是现在还没有初始化对象,其他线程访问if(oneDemo == null)不为空了就直接返回了对象,但是这个对象并没有初始化就出现了问题。

解决:

1、在方法体上加上volatile让这段代码不被指令重拍

2、正确的编程方式,只有确认上面三步都执行完成后再去返回对象

4. 通信

上面所说的步骤其实就是实现了线程之间的通信,但是不要以为线程之间的通信就是这么简单的,其实在Java中JMM内存模型定义了八种操作来实现同步的细节。

  • read 读取,作用于主内存把变量从主内存中读取到本本地内存。
  • load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
  • use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
  • assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
  • lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

所以看似简单的通信其实是这八种状态来实现的。

同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:

  • 不允许read和load、store和write的操作单独出现。
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

所以上面说的操作要严格执行。

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
1月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
13天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
3月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
70 0
|
12天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
33 2
|
3月前
|
存储 缓存 Java
Java内存模型(JMM)
Java内存模型(JMM)是一个抽象概念,用于规范程序中各种变量(实例字段、静态字段及数组元素)的访问方式,确保不同Java虚拟机(JVM)上的并发程序结果一致可靠。JMM定义了主存储器(所有线程共享)与工作存储器(线程私有)的概念,线程间通过主存储器进行通信。JMM具备三大特性:原子性(确保基本读写操作的不可分割)、可见性(确保一个线程对共享变量的修改对其他线程可见)、有序性(防止指令被处理器或编译器重排序影响程序逻辑)。通过这些特性,JMM解决了多线程环境下的数据一致性问题。
|
3月前
|
安全 Java 程序员
深入浅出Java内存模型:探索JMM的奥秘
在Java编程世界中,理解其内存模型(JMM)是提升代码性能和确保线程安全的关键。本文将带你走进Java内存模型的大门,通过浅显易懂的方式揭示其工作原理,并指导你如何在实际开发中有效利用JMM来避免常见的并发问题。
|
4月前
|
存储 安全 Java
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
109 13
|
4月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其在并发编程中的应用
【7月更文挑战第8天】本文旨在探索Java内存模型(JMM)的奥秘,揭示它在并发编程中的关键作用。通过深入浅出的方式,我们将了解JMM的基本概念、关键特性,以及它如何影响多线程程序的行为。文章将带领读者从理论到实践,探讨JMM对编写高效、可靠并发应用的重要性,并展示如何利用这些知识解决实际问题。
70 7
|
4月前
|
Java 程序员 编译器
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
Java面试题:解释Java内存模型(JMM)是什么,它为何重要?
71 2
|
4月前
|
存储 安全 算法
深入理解Java内存模型(JMM)
在Java的并发编程领域,内存模型是一个不可忽视的核心概念。它定义了多线程环境下变量的访问规则,影响着程序的正确性和性能。本文将探讨Java内存模型(JMM)的基本结构、工作原理及其对编写高效、线程安全代码的重要性。