1. 简述
感觉最近看JAVA的内存模型相关的东西看的有点晕,加上前看操作系统也接触到操作系统层面也有内存模型的概念,感觉操作系统的都看懂了java的还是没有整太明白这里尝试对思考的东西做一个总结,希望后面能够不断补充。
在这里尝试对内存模型做一个定义和解释。java官方这里有对内存模型的一个基本定义,不知道怎么翻译才好,我觉得内存模型是一组规则,这一组规则定义了程序对内存中的单个或多个变量操作之间的可见性应该是怎么样的。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。
THE MEMORY MODEL DETERMINES WHAT VALUES CAN BE READ AT EVERY POINT IN THE PROGRAM
内存模型存在的原因:
- 指令重排序
- 缓存主存不一致
这两个原因导致了,在多线程对数据进行竞争的时候可能出现一些不确定的执行结果,也就是程序可能输出A也有可能输出B,全看具体执行的情况,这样的话程序就不具备最终一致性了,所以可定是不行的。所以内存模型主要还是对数据竞争的情况下数据的一致性作出的一些规范。
2. JAVA 内存模型的规则
2.1. 对线程的非共享变量不做任何处理
对线程的非共享变量不做任何处理。也就是线程方法栈内的变量,这些变量不能被外部线程访问到,所以不需要做太多同步性要求,这些变量由线程内的语义控制就行,也就是满足线程内数据一致性AS-IF-SERIAL 这里有一些概念介绍。
2.2. 线程共享变量提供同步机制
对于共享变量,提供的有同步的机制,但是这些不是自动同步的,需要使用同步语义VOLATILE,SYNCHRONIZED
去使用这些同步规则 ,在没有使用任何同步机制的情况下,JAVA内存模型只保证线程内的数据一致性(也称为AS-IF-SERIAL),也就是在一个线程内,能够保证对同一个变量的读写是保持严格的程序逻辑执行顺序的
下面这个指令无法被重排序,因为第二条依赖了第一条。
INT A=5
INT B=A
但是对于不同内存的操作,JMM是允许的
比如下面的这个,在执行的时候是可能改变的
THREAD 1
1: R2 = A;
2: B = 1;
执行的时候可能会变成这样
THREAD 1
2: B = 1;
1: R2 = A;
这样的调整是不影响JMM规范的,但是考虑下面这种情况,假如两个线程并发的话,就会出现数据问题
THREAD 1 THREAD 2
1: R2 = A; 3: R1 = B;
2: B = 1; 4: A = 2;
可能会出现R2 == 2 AND R1 == 1
所以对于共享变量的操作JMM提供了一些约束规范来进行保证共享数据的一致性
2.2.1 同步顺序
JMM规定了一些同步操作
,这些同步之间对应的需要保证同步顺序
,也就是保证程序在执行这些操作的顺序性。
- 对于任意单个线程来说,同步顺序主要是规定了在单个线程内程序内的同步操作的顺序必须按照书写的顺序执行(不会进行重排序)。
- 对于多个线程来说这些操作之间具有先后的顺序(不能同时执行),也就是要满足互斥性。同时后面可以结合
HAPPENS-BEFORE
保证内存的可见性。
对应的同步操作有
1.VOLATILE READ. A VOLATILE READ OF A VARIABLE.
2.VOLATILE WRITE. A VOLATILE WRITE OF A VARIABLE.
3.LOCK. LOCKING A MONITOR
4.UNLOCK. UNLOCKING A MONITOR.
5.THE (SYNTHETIC) FIRST AND LAST ACTION OF A THREAD.
6.ACTIONS THAT START A THREAD OR DETECT THAT A THREAD HAS TERMINATED (§17.4.4).
对应的同步顺序有
- 监视器M上的解锁动作与M上的所有后续锁定动作是需要被同步的,就是按照固定的顺序(其中“后续”根据同步顺序定义)。
- 对易失性变量V(第8.3.1.4节)的写操作与任何线程对V的所有后续读取进行同步(其中“后续”是根据同步顺序定义的)。
- 启动线程的动作与它启动的线程中的第一个动作同步。
- 将默认值(零,FALSE或NULL)写入每个变量与每个线程中的第一个动作同步。 尽管在分配包含变量的对象之前将默认值写入变量似乎有些奇怪,但是从概念上讲,每个对象都是使用默认初始化值在程序开始时创建的。(这一条还是有点晕的,应该是指当前线程栈帧用到的变量吧)
- 线程T1中的最终操作与另一个线程T2中检测到T1已终止的任何操作同步。 T2可以通过调用T1.ISALIVE()或T1.JOIN()来实现。
- 如果线程T1中断了线程T2,则T1的中断将与任何其他线程(包括T2)确定T2已被中断的任何点进行同步(通过引发INTERRUPTEDEXCEPTION或调用THREAD.INTERRUPTED或THREAD.ISINTERRUPTED)。
2.2.2. HAPPENS-BEFORE
HAPPENS-BEFORE 主要强调了可见性,如果A HAPPEN-BEFORE B, 那么A对共享内存所有的操作对B来说都是可见的。
对应的HAPPEN-BEFORE有以下几种
- 监视器的解锁HAPPEN-BEFORE对该监视器的后续加锁,也就是基于SYNCHRONIZED的代码块对内存的操作对于后续进入该内存的操作都是可见的
- 对VOLATILE变量的写HAPPEN-BEFORE对该变量的后续读
- 对线程START()方法的调用HAPPEN-BEFORE该线程内的任何操作
- 一个线程内的所有操作HAPPEN-BEFORE其他线程成功JOIN()该线程
- 任意对象的默认初始化HAPPEN-BEFORE程序的其他操作
上面5条HAPPEN-BEFORE规则涵盖了多线程编程中的锁、共享变量读写、线程生命周期和对象初始化等等重要内容,普通开发人员不用深入了解JMM,只需要知道这5条规则,可以很轻松的处理多线程场景。
以上总结也说不上特别准确,觉得网上的资料众说纷纭,希望后面能看到更权威的解释。
参考
HTTPS://DOCS.ORACLE.COM/JAVASE/SPECS/JLS/SE8/HTML/JLS-17.HTML#JLS-17.4
HTTPS://MONKEYSAYHI.GITHUB.IO/2017/12/28/%E4%B8%80%E6%96%87%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C/
HTTPS://WWW.ZHIHU.COM/QUESTION/296949412 (第三个回答)
HTTPS://JUEJIN.IM/POST/5A2B53B7F265DA432A7B821C
https://ljalphabeta.gitbooks.io/a-primer-on-memory-consistency-and-cache-coherenc/content/
https://legacy.gitbook.com/book/ljalphabeta/a-primer-on-memory-consistency-and-cache-coherenc/details
https://blog.csdn.net/javazejian/article/details/72772461
https://www.jianshu.com/p/6745203ae1fe
(这个是里面有对应的pdf)