Java内存模型

75 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

JMM除了堆栈方法区的划分方式外,在线程与变量的角度上可以将其划分主内存 Main Memory工作内存 Work Memory,其分别是线程共享与线程独占的。可以理解为主内存是 Heap 中对象实例的数据部分,工作内存是 JVM Stack 中的部分区域。

内存间的相互交互

内存模型

程序运行时面对的是Work Memory,因此MM与WM之间必定要制定变量传递方式,这些变量传递必须是原子性的,不可再分。

  1. lock锁定:作用MM,标识某变量线程独占

  2. unlock解锁:作用MM,标识lock的变量释放

  3. read读取:作用MM,将MM的值传递到WM中

  4. load载入:作用WM,将WM的值放入变量副本中

  5. use使用:作用WM,将值传递给执行引擎

  6. assign赋值:作用WM,将执行引擎的值赋值给WM的变量

  7. store存储:作用WM,将WM的变量传送到MM中

  8. write写入:作用MM,将变量值写入

要注意这只是 JVM 制定的一个规则,是脱离平台的抽象标准,实际情况需要在实现时结合平台进行具体设计。JVM使用这8种操作来进行内存交互操作,制定如下规则

  • read-loadstore-write 必须成对出现

  • assgin 操作后需要将其 store 回主内存

  • 不允许无故 store

  • 变量只能在 MM 中产生,WM 的变量必须 read

  • lock 操作会清空 MM 中的 cache,引擎使用需要重新 loaduse

  • unlock 前需要完成 store-write

要注意的是这些规则指定的只是顺序,而不是强制要求连续执行,这样在多线程的情况下多操作的原子不能保证。

volatile修饰符

volatile语义有两种:可见性、防止指令重排。主要由于在写操作后增加lock前缀指令,处理器收到该指令后执行两件事

  1. 当前处理器缓存行数据写会系统内存。

  2. 写回操作使其他CPU内的缓存该地址的数据无效。

这相当于要求对cache中的变量做了一次 store-write 操作。 这保证了 volatile 变量的可见性当一条线程修改了这个变量的值,新值对于其他线程来说立即得知。在JMM的层面要求 use-loadassgin-stroe 操作必须成对且连续,这保证了每次都从缓存行中获取可能的新值。

但这个操作并不是原子的:这就造成了可能线程 T1T_1 更新变量 varvarstore 操作执行完后被 T2T_2 打断,T2T_2 进行了 read 操作,这样读取到的是旧数据,因此 volatile 并不具备原子性。

另外 volatile 添加的 lock 是一个内存屏障,这就限制了内存重排的能力。

long与double型变量

32位的机器没有一次性写入64位数据的能力,因此double和long型数据在32位机器上不是原子性的。JVM对其有着宽松的 Nonatomic Treatment of double and long Variavles 协定。但这个问题随着64位机的来临不再存在了。