共计 2695 个字符,预计需要花费 7 分钟才能阅读完成。
当问到 Java 内存模型的时候,肯定要留神,Java 内存模型(Java Memory Model,JMM)它和 JVM 内存布局(JVM 运行时数据区域)是不一样的,它们是两个齐全不同的概念。
1. 为什么要有 Java 内存模型?
Java 内存模型存在的起因在于解决多线程环境下并发执行时的内存可见性和一致性问题。在古代计算机系统中,尤其是多处理器架构下,每个处理器都有本人的高速缓存,而主内存(RAM)是所有处理器共享的数据存储区域。当多个线程同时拜访和批改同一块共享数据时,如果没有适当的同步机制,就可能导致以下问题:
- 可见性:一个线程对共享变量所做的批改可能不会立刻反映到另一个线程的视角中,因为这些批改可能只存在于本地缓存中,并未刷新回主内存。
- 有序性:编译器和处理器为了优化性能,可能会对指令进行重排序,这可能导致程序在单线程环境中看似依照源代码程序执行,但在多线程环境中的理论执行程序却与预期不同。
- 原子性:即便是最简略的读取或赋值操作,在硬件层面也不肯定保障是原子性的,即在没有同步的状况下,多线程下可能看到操作只执行了一部分的后果。
Java 内存模型通过定义一套规定来标准并限度编译器、运行时以及处理器对内存拜访的重排序行为,确保了多线程间的交互具备明确的语义。它规定了共享变量的拜访规定、提供了 happens-before 准则以及 volatile 关键字、synchronized 等工具来实现内存可见性和一致性的保障。这样,程序员在编写并发代码时,能够根据这些规定来确保代码的正确执行,从而防止因为多线程带来的不确定性和谬误。
如果没有 Java 内存模型就会呈现以下两大问题:
- CPU 和 内存一致性问题。
- 指令重排序问题。
具体内容如下。
1.1 一致性问题
要讲明确缓存一致性问题,要从计算机的内存构造说起,它的构造是这样的:
所以从下面能够看出计算机的重要组成部分蕴含以下内容:
- CPU
- CPU 寄存器:也叫 L1 缓存,一级缓存。
- CPU 高速缓存:也叫 L2 缓存,二级缓存。
- (主)内存
当然,局部高端机器还有 L3 三级缓存。
因为主内存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存(L2)来作为主存和处理器之间的缓冲,CPU 将罕用的数据放在高速缓存中,运算完结后 CPU 再讲运算后果同步到主内存中,这样就会导致多个线程在进行操作和同步时,导致 CPU 缓存和主内存数据不统一的问题。
1.2 重排序问题
因为有 JIT(Just In Time,即时编译)技术的存在,它可能会对代码进行优化,比方将本来执行程序为 a -> b -> c 的流程,“优化”成 a -> c -> b 了,但这样优化之后,可能会导致咱们的程序在某些场景执行出错,比方单例模式双重效验锁的场景,这就是典型的善意办好事的事例。
2. 定义
Java 内存模型(Java Memory Model,简称 JMM)是一种标准,它定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作形式,即 标准了 Java 虚拟机与计算机内存之间是如何协同工作的。具体来说,它规定了一个线程如何和何时能够看到其余线程批改过的共享变量的值,以及在必须时如何同步地访问共享变量。
3. 标准内容
Java 内存模型次要包含以下内容:
- 主内存(Main Memory):所有线程共享的内存区域,蕴含了对象的字段、办法和运行时常量池等数据。
- 工作内存(Working Memory):每个线程领有本人的工作内存,用于存储主内存中的数据的正本,线程只能间接操作工作内存中的数据。
- 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将批改后的数据刷新到主内存。
- 原子性(Atomicity):JMM 保障根本数据类型(如 int、long)的读写操作具备原子性,即不会被其余线程烦扰,保障操作的完整性。
- 可见性(Visibility):JMM 确保一个线程对共享变量的批改对其余线程可见。这意味着一个线程在工作内存中批改了数据后,必须将最新的数据刷新到主内存,以便其余线程能够读取到更新后的数据。
- 有序性(Ordering):JMM 保障程序的执行程序依照肯定的规定进行,不会呈现随机的重排序景象。这包含了编译器重排序、处理器重排序和内存重排序等。
Java 内存模型通过以上规定和语义,提供了一种对立的内存拜访形式,使得多线程程序的行为可预测、可了解,并帮忙开发者编写正确和高效的多线程代码。开发者能够利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程平安和数据一致性。
内存模型的简略执行示例图如下:
3.1 主内存和工作内存交互标准
为了更好的管制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现(以下内容只须要简略理解即可):
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,开释后的变量才能够被其余线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作应用
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中失去的变量值放入工作内存的变量正本中。
- use(应用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要应用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接管到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。
write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
PS:工作内存也就是本地内存的意思。
3.2 什么是 happens-before 准则?
happens-before(后行产生)准则是 Java 内存模型中定义的用于保障多线程环境下操作执行程序和可见性的一种重要伎俩。
举个例子来说,例如 A happens-before B,也就是 A 线程早于 B 线程执行,那么 A happens-before B 能够保障以下两项内容:
- 可见性:B 读取到 A 最新批改的值(通过内存屏障)。
程序性:编译器优化、处理器重排序等因素不会影响先执行 A 再执行 B 的程序。
课后思考
JMM 和内存屏障有什么关系?happens-before 准则和内存屏障有什么关系?内存屏障的类型又有哪些?