JMM 的全称是 Java Memory Model(Java内存模型)
JMM 的关键技术点都是围绕着多线程的 原子性、可见性 和 有序性 来建立的,这也是 Java 解决多线程并行机制的环境下,定义出的一种规则,意在保证多个线程间可以有效地、正确地协同工作。
JMM 关于同步的规定:
?? 全面理解Java内存模型(JMM)及volatile关键字
JMM规定了内存主要划分为 主内存 和 工作内存 两种。
此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来, 主内存对应的是Java堆中的对象实例部分 , 工作内存对应的是栈中的部分区域 。
从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
由上面的交互关系可知,关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。
2-1. Java内存模型定义了以下八种操作来完成:
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述两个操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
2-2. Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
这8种内存访问操作很繁琐,后文会使用一个等效判断原则,即先行发生(happens-before)原则来确定一个内存访问在并发环境下是否安全。
● 原子性:
JMM保证的原子性变量操作包括read、load、assign、use、store、write,而long、double非原子协定导致的非原子性操作基本可以忽略。
如果需要对更大范围的代码实行原子性操作,则需要JMM提供的lock、unlock、synchronized等来保证。
● 可见性:
每个工作线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。
volatile关键字要求被修改之后的变量要求立即更新到主内存,每次使用前从主内存处进行读取。因此volatile可以保证可见性。
除了volatile以外,synchronized和final也能实现可见性。synchronized保证unlock之前必须先把变量刷新回主内存。
final修饰的字段在构造器中一旦完成初始化,并且构造器没有this逸出,那么其他线程就能看到final字段的值。
● 有序性:
java的有序性跟线程相关。如果在线程内部观察,会发现当前线程的一切操作都是有序的。
如果在线程的外部来观察的话,会发现线程的所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。
volatile 和 synchronized 可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到 volatile 和 synchronized 也能保证指令不进行重排序。
关键字 volatile 是 JVM 中最轻量的同步机制。volatile 变量具有 2 种特性:
volatile 语义并不能保证变量的原子性。对任意单个 volatile 变量的读/写具有原子性,但类似于 i + +、i - - 这种复合操作不具有原子性,因为自增运算包括读取i的值、i 值增加 1、重新赋值 3 步操作,并不具备原子性。
由于 volatile只能保证变量的可见性和屏蔽指令重排序 ,只有满足下面2条规则时,才能使用volatile来保证并发安全,否则就需要加锁(使用 synchronized、lock 或者 java.util.concurrent 中的 Atomic 原子类)来保证并发中的原子性。
因为需要在本地代码中插入许多内存屏蔽指令在屏蔽特定条件下的重排序,volatile 变量的写操作与读操作相比慢一些,但是其性能开销比锁低很多。
代码演示:
- 保证可见性
public class JmmTest01 { // volatile 可以保证可见性 public volatile static int num = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { // 线程 1 对主内存的变化不知道的 while (num == 0) { System.out.println(Thread.currentThread().getName() + " run >->->"); } }).start(); TimeUnit.SECONDS.sleep(1); num = 1; System.out.println(num); } }
- 不保证原子性
public class JmmTest02 { // volatile 不保证原子性 public volatile static int num = 0; public static void add() { num++; // 不是一个原子性操作 } public static void main(String[] args) { // 理论上 num 应该等于 100000 for (int x = 0; x < 100; x++) new Thread(() -> { for (int i = 0; i < 1000; i++) add(); }).start(); while (Thread.activeCount() > 2) { // main gc Thread.yield(); } // main - 99608 System.out.println(Thread.currentThread().getName() + " - " + JmmTest02.num); } }
保证原子性:如果不加 lock 和 synchronized ,则使用 原子类 解决原子性问题!!!
public class JmmTest02 { // 原子类的 Integer public static AtomicInteger num = new AtomicInteger(); public static void add() { num.getAndIncrement(); // AtomicInteger + 1 方法, CAS } public static void main(String[] args) { // 理论上 num 应该等于 100000 for (int x = 0; x < 100; x++) new Thread(() -> { for (int i = 0; i < 1000; i++) add(); }).start(); while (Thread.activeCount() > 2) { // main gc Thread.yield(); } // main - 100000 System.out.println(Thread.currentThread().getName() + " - " + JmmTest02.num); } }
这些原子类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe 类是一个很特殊的存在!
什么是 指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。从硬件架构上来说, 指令重排序是指CPU采用了允许将多条指令不按照程序规定的顺序,分开发送给各个相应电路单元处理,而不是指令任意重排 。重排序分成三种类型:
处理器在进行指令重排的时候,考虑:数据之间的依赖性!
int x = 1; // 1 int y = 2; // 2 x = x + 5; // 3 y = x * x; // 4 // 我们所期望的:1234 但是可能执行的时候回变成 2134 1324,可不可能是 4123!
重排代码实例:
声明变量:int a,b,x,y=0
线程A | 线程B |
x=a | y=b |
b=1 | a=2 |
结果 | x = 0; y = 0; |
如果编译器对这段程序代码执行重排优化后,可能出现如下情况:
线程A | 线程B |
b=1 | a=2 |
x=a | y=b |
结果 | x = 2; y = 1; |
这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象
避免指令重排:Volatile
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:
由于编译器和处理器都能执行指令重排优化。如果在指令前插入一条Memory Barrier,则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
原文:https://www.cnblogs.com/Dm920/p/13362306.html