本文主要介绍一下JMM中的一些常见概念,通过本文让你能够快速的对JMM有一个大致的了解
Java内存模型是什么?
Java内存模型一般简称 JMM(Java Memeory Model),是为了规范 Java 程序多线程之间共享数据的访问规则而定义的。它主要关注程序中的变量在内存中的可见性、顺序性和操作原子性等方面的问题。Java内存模型定义了一系列的规则和保证,用于确保多线程程序中的共享变量能够正确、可靠地被访问和修改。
温馨提示:一定不要和 JVM内存结构、JVM内存模型、Java内存模型三个概念给搞混了,看网上好多人直接认为 JVM 内存模型就算 JMM,三者之间的区别:
总结:JVM内存结构是JVM内存模型的子集,JVM内存模型侧重概念,Java内存模型侧重规范


什么需要JMM?
因为多线程开发中会存在很多问题,比如共享数据的可见性问题、指令重排等问题,为了解决这些问题,就需要制定一些规范,也就是所谓的内存模型。一般来说,编程语言也可以直接复用操作系统层面的内存模型。不过,不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java的早期创建者为了确保 Java程序的可移植性,就直接设计了一套专属于 Java 的内存模型(规范),Java开发者只需要遵循这一套,就可以编写正确、可靠且高效的多线程程序,避免常见的并发问题,确保数据的一致性和线程安全性。
总结:为了解决多线程种数据的一致性问题、指令重排问题,Java提供了一套规范也就是 JMM,通过这套规范,Java开发者在不需要了解底层原理就能很简单(直接使用并发相关的一些关键字和类)解决Java多线程开发种的一些问题。
JMM的作用?
JMM 说白了就是定义了一些规范来解决并发编程中的常见问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如
volatile、synchronized、各种Lock)即可开发出并发安全的程序
总结:解决多线程开发种遇到的指令重排、可见性问题
什么是指令重排?
指令重排(Instruction Reordering)是指编译器或处理器在执行程序时,为了优化性能而改变原始指令序列的顺序。这种重排可能会导致代码的执行顺序与源代码中编写的顺序不一致。
为什么需要指令重排?
指令重排的出现是为了提高程序的执行效率和性能。处理器或编译器会尽量利用现代计算机体系结构的特性,如流水线执行、乱序执行、寄存器重命名等技术进行优化。通过重排指令,可以更好地利用处理器的资源,减少潜在的数据依赖和等待时间,从而加快程序的执行速度。
导致指令重排的原因有哪些?
总结:导致指令重排序的原因无非分为三大类
Java 源代码会经历 编译器优化重排 → 指令并行重排 → 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。
注意:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
什么是主存?
主存在 JMM 中是一个逻辑上的概念1,它定义了多线程程序中共享变量的可见性和一致性规则,要求所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
注意:不要将 JMM 中的 ”主存“ 这个逻辑概念和平常所说的”主存“这个物理概念搞混了,物理概念上的主存是指RAM(随机存取器),也称”内存“
什么是本地内存?
每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存(也称工作内存),无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
注意:线程对于共享变量的读写都是发生在工作内存中的
JMM为什么要划分主存和内存?
在 JDK1.2之前 ,Java虚拟机(JVM)使用的是主存模型。在主存模型中,所有线程都直接读写主存中的共享变量,这样做会产生一些问题,例如,由于缓存一致性协议2的存在,不同的处理器可能会有自己的缓存,导致线程间无法及时看到对共享变量的修改,导致出现数据一致性问题。此外,编译器也可能对代码进行指令重排序,进一步破坏多线程之间的一致性。
在 JDK1.2时,Java引入了本地内存这个概念概念,本地内存位于每个线程的工作内存中,用来存储线程私有的数据。当一个线程访问共享变量时,首先会将共享变量从主存复制到本地内存中进行操作,然后再将修改后的值刷新回主存。借助本地内存,可以解决缓存一致性问题。当一个线程修改了本地内存中的共享变量后,其他线程可以通过主存来感知到这个变化。此外,通过限制对本地内存的访问,可以避免编译器过度优化和指令重排序,从而保证多线程程序的正确执行。
添加了本地内存后读写共享变量的具体流程:
前面介绍了 JMM 是什么?说到 JMM 就是一套规范,而这套规范的核心就算 8种同步规则 和 8种同步操作,通过遵循这8种同步规则和8种同步操作,我们就能够很大程度上避免多线程开发中出现数据一致性问题和可见性问题。本小节我们将介绍以下这8种同步规则和8种同步操作
8种同步规则
8种同步操作
锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
Java官方基于这8种同步规则和8种同步操作进行内部实现,我们平常使用的如:synchronized关键字、volatile修饰符、Lock等都是Java官方提供的内部实现,他们都遵循了这两个8。
那么是不是我们这些 API 调用工程师是不是就只需要使用这些提供好的内部实现即可,不用理会这两个8?非也非也,仅仅依赖于内部实现是不够的。作为开发人员,了解并遵守JMM的同步规则以及合理地使用同步操作是非常重要的,以确保编写出具有正确并发行为的多线程程序。通过遵循这些规则和操作,可以避免潜在的并发问题,例如数据竞争、死锁、活锁等。此外,合理地使用同步机制还能提高程序的性能和效率。
happens-before原则是什么?
happens-before 原则是用于描述多线程程序中操作之间的时间顺序关系的一套规则,具体可以分为8种不同的规则。
happens-before原则的包含的八种规则
程序顺序规则(Program Order Rule):在同一个线程中,按照程序代码的先后顺序执行的操作,前一个操作的结果对后续操作可见。
锁定规则(Lock Rule):一个释放锁的操作(unlock)happens-before 后续相同锁的获取锁操作(lock)。这意味着,在解锁之前对共享变量所做的修改对于后续获取该锁的线程是可见的。
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写入操作happens-before 后续对该变量的读取操作。这保证了对volatile变量的修改对所有线程是可见的。
传递性规则(Transitive Rule):如果操作A happens-before 操作B,且操作B happens-before 操作C,则操作A happens-before 操作C。这个规则可以推导出更复杂的happens-before关系。
线程启动规则(Thread Start Rule):一个线程的启动操作happens-before 该线程中的任何操作。
线程终止规则(Thread Termination Rule):一个线程中的任何操作happens-before 其他线程检测到该线程已经终止的操作。
线程中断规则(Thread Interruption Rule):对于任意线程,调用interrupt()方法的操作happens-before 被中断线程检测到中断事件的操作。
对象终结规则(Finalizer Rule):一个对象的构造函数执行happens-before 该对象的finalize()方法。
happens-before原则的作用是什么?
- 保证可见性:根据happens-before原则,如果一个操作A happens-before另一个操作B,那么操作A的结果对操作B来说是可见的。这意味着,通过正确使用同步机制(如volatile、synchronized、Lock等),可以确保对共享变量的修改对其他线程是可见的,从而避免了数据不一致性和竞态条件。
- 确保顺序性:happens-before原则定义了操作之间的时间顺序关系,可以确保按照预期的顺序执行操作。例如,一个线程的启动操作happens-before该线程中的任何操作,这样就可以确保子线程中的操作在子线程开始运行之前完成。
- 避免编译优化问题:happens-before原则还能够防止编译器和处理器对指令进行重排序,以保证指令的执行顺序符合预期。这样可以避免由于编译优化导致的程序行为异常。
- 提供并发安全性:通过happens-before原则,程序员可以使用同步机制来创建临界区,保护共享资源的访问和修改。这样可以避免多线程并发访问共享资源时出现不一致的情况,提供并发安全性。
主要作用是确保操作的顺序性,当满足的操作的顺序性时,自然可以满足可见性和指令重排等问题
happens-before原则和JMM的关系是什么?
JMM是对于多线程开发的一套规范,happens-before原则是JMM呈现给程序员的抽象视图,让程序员通过合理地设置操作之间的 happens-before 关系,来确保代码在多线程环境下的正确性,从而不必要过度关注底层的实现。

什么是内存屏障?
内存屏障(Memory Barrier),也称为内存栅栏,是一种硬件或软件指令,用于控制处理器和内存之间的操作顺序和可见性。
为什么需要内存屏障?
在多线程并发执行的环境下,由于处理器和内存的优化机制,可能会对代码的执行顺序进行重排序,导致线程间的可见性和有序性问题。为了解决这些问题,内存屏障被引入。
JMM和内存屏障的关系
内存屏障是实现 JMM 的一种手段。JMM 规定了编译器和处理器如何通过插入内存屏障来确保多线程间操作的顺序性和可见性。内存屏障可以控制指令重排序和数据在处理器缓存和主内存之间的同步,从而保证了线程间的互相可见性和正确的执行顺序。
在 JMM 中,通过使用
volatile关键字或锁机制(如synchronized和Lock)来实现内存屏障的效果。当一个线程通过volatile变量或锁来与主内存进行交互时,会自动插入适当的内存屏障,以确保变量的可见性和有序性。
内存屏障的分类
内存屏障的作用
参考资料