# JMM
# 0. 原子性、可见性、有序性
并发编程中引起非线程安全的主要原因是共享变量的原子性、可见性、有序性。
- 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行。
对共享变量操作不满足原子性,多线程访问会造成数据不一致。
- 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
Java为了解决重排序、原子性、可见性问题,提供了语言级别的两个关键字synchronized和volatile。
volatile是Java内存模型提供的最轻量级的同步机制,它可以保证多线程操作共享变量的有序性以及内存可见性。
synchronized关键字则是一个互斥锁,可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
# 双重校验的例子:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
由于 instance = new Singleton()不是原子性,需要三个JVM指令,
- 分配内存
- 初始化
- 赋值
由于指令重排序,导致先赋值,后初始化,其他线程拿到不是null但没有初始化的instance。而将instance用volatile修饰可以禁止指令重排序,从而解决该问题。
# 1.概述
JAVA内存模型(Java Memory Model)简称JMM。
JMM是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
JMM的主要目标是定义程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。
共享变量主要指实例变量
JMM主要影响共享变量在主内存和工作内存的一致性问题。
JMM将内存分为两种:
- 主内存(Main Memory)
- 本地内存(Working Memory)或工作内存
每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
JMM对多线程对于共享变量的读写机制规定
- 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量传递需要经过主内存同步
- 如何做数据同步以及什么时候数据同步。
本地内存是一个抽象概念,统一了缓存,缓冲区,寄存器等硬件以及编译器优化等。
# happens before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
- 1 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
- 2 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
- 3 volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
- 4 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
- 线程启动规则:在线程上调用start()方法,必须在该线程执行任何操作之前执行。
- 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已结束之前执行。
- 终结器规则:对象的构造函数必须在启动该对象终结器之前执行完成。
- 中断规则:对线程 interrupt方法的调用,happens-before被中断线程的代码检测到中断事件的发生。
两个操作之间存在happens-before关系,并不意味着一定要按照 happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before关系来执行的结果一致,那么这种重排序并不非法。
# as-if-serial
as-if-serial语义的是:所有的操作均可以为了优化而被重排序,但是必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial语义。
注意,as-if-serial 只保证单线程环境,多线程环境下无效。
# final
对于final域,编译器和处理器要遵守两个重排序规则。
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
# 2. 指令重排序
编译器或处理器为了提高处理性能会对指令进行重排序。
满足条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序
重排序分类:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
JMM处理 对于编译器重排序,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。 对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
4、内存屏障
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
- 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。 内存屏障有两个作用:
阻止屏障两侧的指令重排序; 强制把写缓冲区/高速缓存中的数据等写回主内存,让缓存中相应的数据失效。
# 8种内存指令
- lock:标识为独占状态
- unlock:释放独占状态
- read:主内存到工作内存
- load:加载到工作内存的变量副本
- use:传递给执行引擎执行指令使用
- assign:从执行引擎赋值到工作内存
- store:工作内存到主内存
- write:放入主内存变量
内存模型能保证这8个指令是原子性的,但没有办法保证这些指令组合在一起的原子性。
内存模型规定,把一个变量从主内存复制到工作内存中,就要顺序的执行read和load操作,如果要把变量同步回主内存,就要顺序的执行store和write操作。
# 后序
JMM对开发者影响主要体现在多线程编程模块,可以理解为了屏蔽CPU缓存对多线程的影响设计的模型。
线程间通信的一种方法就是共享内存,共享内存就是不同线程访问同一内存进行信息共享。理解JMM才能理解内存可见性,正确进行线程间通信。
JMM规范了线程对内存的读写操作,从而保证不同环境下指令执行的正确性。