感觉最近看JAVA的内存模型相关的东西看的有点晕,加上之前看操作系统也接触到操作系统层面也有内存模型的概念。在这里尝试对内存模型做一个定义和解释。这里有对内存模型的一个基本定义,不知道怎么翻译才好,我觉得内存模型是一组规则,这一组规则定义了程序对内存中的单个或多个变量操作之间的可见性应该是怎么样的。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。
THE MEMORY MODEL DETERMINES WHAT VALUES
CAN BE READ AT EVERY POINT IN THE PROGRAM
内存模型存在的原因:
- 指令重排序
- 缓存主存不一致
JAVA 内存模型的规则
1. 对线程的非共享变量不做任何处理
对线程的非共享变量不做任何处理。也就是线程方法栈内的变量,这些变量不能被外部线程访问到,所以不需要做太多同步性要求,这些变量由线程内的语义控制就行,也就是满足线程内数据一致性AS-IF-SERIAL 这里有一些概念介绍。
2. 线程共享变量提供同步机制
1. 线程内的数据一致性保证
对于共享变量,提供的有同步的机制,但是这些不是自动同步的,需要使用同步语义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. 同步方式
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)。
3. 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条规则,可以很轻松的处理多线程场景。
对于内存中的共享变量,JMM提供了HAPPEN-BEFORE规则来约束指令的重排序和缓存内存的可见性
好博客
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/
VOLATILE 和 MESI的关系,是利用了不同的系统去实现VOLATILE,中间隔着好几层
HTTPS://WWW.ZHIHU.COM/QUESTION/296949412
HTTPS://JUEJIN.IM/POST/5A2B53B7F265DA432A7B821C
JAVA JMM和操作系统的内存模型,他是根据不同的操作系统特性做了实现
HTTPS://WWW.ZHIHU.COM/QUESTION/296949412 第3条
VOLATILE 的JMM实现
当然VOLATILE还有一个作用是禁止了指令重排序,和内存屏障是不一样的,指令重排序是指单个线程中,CPU执行的指令是按照顺序执行的,内存屏障是指多个核同时运行的时候对主内存的读写的顺序性或者说是可见性。
JMM使用上面四种屏障提供了JAVA程序运行时统一的内存模型,以VOLATILE为例:
1)VOLATILE变量执行写操作,会在写之前插入STORESTORE BARRIERS,在写之后插入STORELOAD BARRIERS。 解释:VOLATILE变量在执行写操作之前插入STORESTORE BARRIERS,代表在执行VOLATILE变量写之前的所有STORE操作都已执行,数据同步到了内存中(将STORE BUFFER中的STORE操作刷新到内存);写之后插入STORELOAD BARRIERS,代表该VOLATILE变量的写操作也会立即刷新到内存中,其他线程会看到最新值。 2)VOLATILE变量执行读操作,会在读之前插入LOADLOAD BARRIERS,在读之后插入LOADSTORE BARRIERS。 解释:VOLATILE变量在执行读操作之前插入LOADLOAD BARRIERS,代表在执行VOLATILE变量读之前的所有LOAD从内存中获取最新值;在读之后插入LOADSTORE BARRIERS,代表该读取VOLATILE变量获得是内存中最新的值。
内存模型的实现,操作系统内存模型实现
HTTPS://LJALPHABETA.GITBOOKS.IO/A-PRIMER-ON-MEMORY-CONSISTENCY-AND-CACHE-COHERENC/CONTENT/%E7%AC%AC%E4%B8%80%E7%AB%A0-CONSISTENCY.HTML
指令重排序和PROGRAM ORDER 是不冲突的,PROGRAM ORDER 是指单线程而不是单核。注意,即使处理器完全按照程序顺序(PROGRAM ORDER)执行指令也可能发生指令重排序哦(译者注:比如第一个原因是前面的指令缓存MISS,而后面的指令缓存HIT。毕竟,缓存是透明的)。对于单线程程序来说,两条对内存不同地址进行写操作的指令发生了指令重排序,是不影响程序结果的。但是,在多线程情况下,比如表3.1, 如果对C1的两条指令发生重排序,就会影响程序的最终结果。并且,写指令重排序是不可预测的,你猜不准何时会发生。用缓存一致性协议时避免不了此问题的(缓
存是透明的)。
内存模型和指令重排序是两码事儿,怎么来做区分呢,内存模型主要是为了解决应为缓存等问题导致了CPU执行的时候看到的数据不一致的问题
指令重排序是指CPU对指令进行了重排,在时间顺序上改变了执行的先后,而内存模型是假设CPU是执行的顺序是正确的,如果保证在正确执行的顺序下保证CPU看到的内存是一致的,所以单核CPU理论上是不需要内存屏障的,因为数据都是在一个CPU上面运算,所以一致性和可见性是没有问题的。
参考这里,我觉得这里说的更有道理
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/
内存屏障兼顾了对内存的可见性处理和指令的重排序约束
内存屏障的作用就是要保证CPU执行了对应的操作以后CPU看到的数据的一致性。
应该说的是内存屏障和指令重排序是两个事儿,而不是说内存模型和指令重排序是两个事儿,因为内存模型的支撑需要指令重排序的一些相关设置,同时内存模型也需要内存屏障的支撑,JAVA的内存屏障应该也是这个意思吧。
感觉JAVA的内存屏障也包括了对指令重排序的功能,同时包含了对缓存的管理功能。
我感觉指令重排序等是内存模型需要处理的问题之一,内存模型主要是为了处理多个线程的CPU看到的变量的值不一致的问题,我们允许这个不一致的情况有多严重,这就是内存模型,为了满足内存模型,什么时候需要对指令重排序进行禁止,什么时候需要锁缓存。这是两个维度的事儿。
那种LOADSTORE的屏障不仅是禁止指令排序,同时需要满足内存的可见性,要满足内存的可见性,首先就不能让指令重排序。
这个链接杰哥
HTTP://IFEVE.COM/COOKBOOK-RECIPES/
感觉内存屏障还是不包括指令重排,这个是JAVA规范的两个方面,JVM实现这两个方面保证使用的措施可能不同,但是某些情况下需要同时进行指令重排序的限制和指令重排序的限制
哪些可以作为常量
HTTPS://BLOG.CSDN.NET/U010798968/ARTICLE/DETAILS/72835255
volatile的一些问题
https://www.jianshu.com/p/6745203ae1fe