先分析下线程访问数据的上述图结构
共享数据存储在主内存中,每个线程访问数据先把共享数据拷贝一份到各自线程的本地内存中,线程运行的数据其实是本地内存中的拷贝数据。线程A对共享变量做出改变后,刷新到本地内存,然后在某个时刻再刷新到主内存(不是立即刷)。线程B再去访问该共享变量,从主内存读取副本到B的本地内存。多线程通过主内存共享变量的方式来达到数据通信。
本地内存
本地内存只是一个概念性的东西,实际并不存在,例如CPU高速缓存,读写缓存器,寄存器,及其他硬件都属于本地内存的范畴
从原子性、可见性、有序性方面分析JMM
原子性
原子性要求同时只有一个线程操作代码,不允许打断,要不执行完全,要不就不执行。
可以通过synchronized和lock来解决
可见性
可见性即内存可见。在分析上述图片流程时,线程A修改数据后,没有立即刷新到主内存,B线程如果在A中共享变量刷新到主内存前去读取,得到的数据就不是最新的。这就是可见性问题。可以通过同步或者volatile解决
有序性
java编译器或者处理器会对指令重排,单线程模式下处理器会保持执行结果的一致性。例如:
class A {}
A a = new A();
创建对象这一步不是原子性操作,是一个复合操作:
1:collect申请内存
2:完成对象初始化
3:将对象堆内存首地址赋值给a
这三个指令在运行时顺序是不可控的。这就是在创建单例时要加volatile的原因,禁止指令重排。
volatile
1:对基本类型成员变量的度和写操作(不是复合操作)保持原子性
2:保持数据在线程间的可见性
3:在一定程度上保持有序性。主要是通过禁止指令重排来达到的。之所以说一定程度上保持,是因为不能完全保持,禁止指令重排volatile对读和写限制是不一样的。
int i; // 1
int j; // 2
int value = i + j; // 3
volatile x = 2; // 4
int max = x; // 5
int index = i; // 6
1)第4处是volatile的写操作。volatile的写操作与它之前的读写操作是禁止重排序的,也就是说1,2,3要发生在4执行之前完全执行完毕,并且对volatile及之后的指令具有可见性,但是6的有序性不能保证,6有可能在1或2后面。根据happen-befores原则,6一定在1后面。
2)第5处是对volatile的读操作。volatile读操作与它之后的读写操作是禁止重排序的,也就是说6要发生在5之后执行。
此处是volatile写操作,因此就可以保证对象创建完全才会发生赋值操作,就避免了问题。
这里有java内存模型的系列文章,非常详细:
https://www.infoq.cn/article/java-memory-model-1
https://www.infoq.cn/article/java-memory-model-2
https://www.infoq.cn/article/java-memory-model-3
https://www.infoq.cn/article/java-memory-model-4
https://www.infoq.cn/article/java-memory-model-5
https://www.infoq.cn/article/java-memory-model-6
https://www.infoq.cn/article/java-memory-model-7