JMM 内存模型、volatile 关键字保证有序性和可见性相关问题总结

简介: 临近秋招,备战暑期实习,祝大家每天进步亿点点!Day19

本篇总结的是 JMM内存模型,volatile 关键字保证有序性和可见性的原理,happens-before原则。

image.png

参考文章:Java面试官告诉你JMM是什么和面什么、阿里实习面经、「阿里面试系列」分析Synchronized原理,让面试官仰望、happens-before理解和应用、面试官:说说什么是 Java 内存模型(JMM)?、面试官:volatile是如何保证可见性和有序性的?、happen-before原则、通俗易懂讲解happens-before原则


1、什么是Java内存模型?

首先,要知道,Java 内存模型指的是 JMM,而不是运行时数据区哦~


Java 语言为了保证并发编程中可以满足原子性、可见性及有序性,于是推出了一个概念就是 JMM 内存模型。


JMM 内存模型,目的是为了在多线程条件下,使用共享内存进行数据通信时,通过对多线程程序读操作、写操作行为规范约束,来尽量避免多次内存数据读取不一致、编译器对代码指令重排序、处理器对代码乱序执行等带来的问题。


JMM 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。

JMM 内存模型将内存主要划分为主内存和工作内存两种。规定 所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,线程的工作内存中保存了该线程所需要用到的变量在主内存中的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读、写主内存。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要线程自己的工作内存和主存之间进行数据交互。

如图所示:


image.png

image.png

JMM 内存模型工作内存、主内存和 JVM 内存有什么关系?


JMM 内存模型中,工作内存和主内存其实跟JVM内存的划分是在不同层次上进行的,是自己的一套抽象概念,大概可以理解为,主内存对应的是 Java 堆中的对象实例部分,而工作内存对应的则是栈中的部分区域。


2、JMM 定义了哪些操作来完成主内存和工作内存的交互操作?

JMM 定义了8 个操作来完成主内存和工作内存的交互操作:


① 首先是从 lock 加锁开始,作用于主内存的变量,把一个变量标识为一条线程独占的状态;

② read 读取,作用于主内存变量,将一个变量的值从主内存读取到工作内存中;

③ load 加载,作用于工作内存的变量,把 read 读取到的值加载到工作内存的变量副本中;

④ use 使用,作用于工作内存的变量,把工作内存中变量的值传递给执行引擎使用,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;

⑤ assign 赋值,作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;

⑥ store 存储,作用于工作内存的变量,把工作内存中变量的值传送回主内存中,以便随后的 write 的操作;

⑦ write 写入,作用于主内存的变量,把 store 得到的值放入主内存的变量中;

⑧ 最后是 unlock 解锁,把主内存中处于锁定状态的变量释放出来,流程到这一步就结束了。

如图所示:

image.png

JMM 基本可以说是围绕着在并发中如何处理这三个特性而建立起来的,也就是原子性、可见性、以及有序性。


3、volatile关键字是如何保证可见性的?

当一个共享变量被 volatile 修饰时,它会保证修改的值会被立即更新到主内存中,当有其他线程读取该值时,也不会直接读取工作内存中的值,而是直接去主内存中读取。


而普通的共享变量不能保证可见性的,因为普通共享变量被修改后,写入了工作内存中,什么时候写入主内存其实是不可知的,当其他线程去读取是,此时无论是工作内存还是主内存,可能还是原来的值,因此无法保证可见性。


被volatile关键字修饰的变量,在每个写操作之后,都会加入一条store内存屏障命令,此命令强制将此变量的最新值从工作内存同步至主内存;在每个读操作之前,都会加入一条load内存屏障命令,此命强制从主内存中将此变量的最新值加载至当前线程的工作内存中。


4、volatile关键字是如何保证有序性的?

volatile 可以禁止指令重排,保证程序会严格按照代码的先后顺序执行。


加了volatile 修饰的共享变量,通过内存屏障解决多线程下的有序性问题。原理如下:


在每个 volatile 写操作的前面插入一个 StoreStore 屏障

在每个 volatile 写操作的后面插入一个StoreLoad屏障

在每个 volatile 读操作的后面插入一个LoadLoad屏障

在每个 volatile 读操作的后面插入一个LoadStore屏障

volatile 在写操作前后插入了内存屏障后生成的指令序列示意图如下:

image.png

volatile 在读操作后面插入了内存屏障后生成的指令序列示意图如下:

image.png

5、happens-before原则

happens-before 的概念:


JMM 可以通过 happens-before 关系向程序员提供跨线程的内存可见性保证(如果 A 线程的写操作 a 与 线程 B的读操作 b 之间存在 happens-before 关系,尽管 a 操作和 b 操作在不同的线程中执行,但 JMM 向程序员保证 a 操作将对 b 操作可见)。


happens-before 具体定义:前面一个操作的结果对后续操作是可见的。


1)如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。


两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。只要两个操作在指令重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序也是合法的。

happens-before 的 8 大原则:


① 单线程happen-before原则:同一个线程中前面的所有写操作对后面的操作可见。

② 锁的happen-before原则:同一个锁的 unlock 操作 happen-before 此锁的 lock 操作。这条规则是指对一个对象的解锁 happen-before 于后续对这个对象的加锁。

③ volatile的happen-before原则:对一个 volatile 变量的写操作 happen-before 对此变量的任意操作(当然也包括写操作了)。

④ happen-before的传递性原则:如果 A 操作 happen-before B 操作,B 操作 happen-before C操作,那么 A 操作happen-before C 操作。

⑤ 线程启动的happen-before原则:同一个线程的 start() 方法 happen-before 于此线程的其它方法。

⑥ 线程中断的happen-before原则:线程 A 新写入的所有变量,当调用 Thread.interrupt(),被打断的线程 B,可以看到 A 的全部操作。

理解:线程 A 写入的所有变量,调用 Thread.interrupt(),被打断的线程 B,可以看到 A 的全部操作。

⑦ 线程终结的happen-before原则:线程中的所有操作都 happen-before 线程的终止检测。

⑧ 对象创建的happen-before原则:一个对象的初始化完成(构造函数执行结束)先于他的 finalize() 方法调用。


相关文章
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
405 0
|
10月前
|
安全 Java 数据库连接
一把锁的两种承诺:synchronized如何同时保证互斥与内存可见性?
临界区指多线程中访问共享资源的代码段,需通过互斥机制防止数据不一致与竞态条件。Java用`synchronized`实现同步,保证同一时刻仅一个线程执行临界区代码,并借助happens-before规则确保内存可见性与操作顺序,从而保障线程安全。
346 11
|
10月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
244 1
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
319 2
|
存储 缓存 Java
【JavaEE】——内存可见性问题
volatile,一个线程读,一个线程写,两个线程互相读,多个线程多把锁
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
289 1
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
3469 0
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
1165 1