文章目录
- 1、JMM的背景
- 2、Java Memory Model
- 3、JMM规范下的三大特性
- 可见性
- 原子性
- 有序性
- 4、多线程对变量的读写过程
- 5、总结
1、JMM的背景
如图,对于磁盘、内存、CPU等硬件,内存和CPU的运行速度不是一个量级的,不能总让CPU等着内存,因此,在CPU和内存之间,出现了一个高速缓存(CPU的一二三级缓存)。
CPU的运行并不是直接操作内存,而是先把内存里边的数据读到高速缓存,再让高速缓存去和内存沟通,此时,就会出现,写完了,但读内存时还没从高速缓存中同步过来的情况,即读写不一致的问题。
基于此,JVM中定义了一种Java的内存模型,Java Memory Model,即JMM,用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台或者操作系统下,都能达到一致的内存访问效果。
2、Java Memory Model
- JMM本身是一种抽象概念,并不真实存在(不是内存条一样的可以摸到)
- JMM是一组约定或规范
- 规范中定义了程序中各个变量的读写访问方式
- 规范中定义了一个线程对共享变量的写入何时以及如何变成对另一个线程可见
- JMM的关键技术点都是围绕多线程的原子性、可见性、有序性展开
JMM作用是:
- 实现CPU(线程)和主内存之间的抽象关系
- 屏蔽硬件和操作系统的内存访问差异,实现Java程序在各个平台下都能达到一致的内存访问效果
3、JMM规范下的三大特性
- 可见性
- 原子性
- 有序性
可见性
指当一个线程修改了某一个共享变量的值,其他的线程能都立即知道该变更,JMM规定了所有的变量都存储在主内存中。
public class Dog{
private int age;
}
现在new一个Dog对象,多个线程共享操作这个对象(比如修改这个对象的age属性),共享变量在主内存中有且仅有一份,如果A线程想把age属性从5改成6,它不能直接去操作主内存,而是先把这个共享变量读到线程A自己所持有的本地内存里,也就是共享变量的副本,改完后再提交写回主内存。现在A线程改完了,我要让其他线程马上知道:我写到主内存了,你们可以去主内存当中读取最新的数据了,这就是可见性。
每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等) 都必须在线程自己的工作内存中进行,而不能够直接读主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
核心词语:工作内存、主内存、主内存副本拷贝
原子性
指一个操作是不可打断的(不可再分,不可打断,所以起名原子),即多线程环境下,操作不能被其他线程干扰。比如A、B两个线程共同操作变量x:
关键点:
- A线程要将更改后的x从自己的工作内存刷回主内存时,被挂起了,没来的及将更新后的值写回主内存
- B线程去读 ,你以为是x=1,B线程再+1应该得2,但最后却从主内存读到0,x+1 = 1
以上的问题,是系统主内存共享变量数据在线程自己的工作内存中修改后,被写入主内存的时机是不确定的,多线程并发下很可能出现"脏读"。加入原子性,工作内存修改完成和写入主内存这两个操作别再分,就不会有这个问题。
有序性
代码从上往下执行,这是基础知识,但为了提升性能,编译器和处理器会对指令序列进行重排序,JVM内部维持顺序化语义,只要程序的最终结果与它顺序化执行的结果一样,那指令的执行顺序可以和代码顺序不一致,即指令的重排序。
指令重排序的优缺点:
- 优点:使得指令更复合CPU特性,最大限度发挥性能
- 缺点:指令重排保证了串行语义的一致,但不保证多线程下的语义一致,可能产生脏读
总之,指令重排是为了程序的性能最佳,比如计算2+1+3,在Linux下运算1+2+3快,而在Windows下运算3+2+1快,二者与最终的计算结果都想到,允许指令重排,但这一切的前提是正确性,所以有的时候要根据特殊场景的业务情况,来禁止指令重排,以保证程序执行的有序性。
public void mySort(){
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x*x; //语句4
}
此时,1234、2134的顺序都行(当然这里没写编译处理后的指令,用代码代替表达含义)。但语句4不能重排后变成第一条了,变量都没定义,怎么用,即数据的依赖性。
4、多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(也称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作读取赋值等)必须在工作内存中进行,所以正确的操作流程是:首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,如下图:
JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存,从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地工作内存,本地内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存)
5、总结
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
最后,JMM对这三大特性保证的底座就是happens-before规则