Java 内存模型与 volatile



感悟

最近看《深入理解 Java 虚拟机》的一些笔记,又是一本相见恨晚的书,强烈推荐前几章和后面几章。

内存模型

image

此图来自《深入理解 Java 虚拟机》,看完以后感觉这个知识水平都提高了2333。这里的主内存不是内存条,而是虚拟机的内存的一部分。不同的线程通过刷新主内存中的值来进行同步。如果没有线程并发的考虑,闭着眼睛就能想到数据的不一致。

内存模型定义了以下 8 种操作来完成主内存与工作内存同步的细节,除去特殊情况,每一个操作是原子不可再分的

  • lock
  • unlock
  • read
  • load
  • use
  • assign
  • store

另外还定义了一些规则,太多了没咋记住……不过也以此引出了最为常见的先行发生的原则,用于确定访问是否线程安全。

happen-before 规则

中文名叫先行发生规则,含义是Java 内存模型下天然存在一些关系,无需同步器去实现,可以在编码中直接使用。换句话说,不在先行发生规则的两个操作,虚拟机可以对它们进行重排序,也就是线程不安全了。

规则有八条:

  • 程序次序规则
    同一线程按照控制流顺序
  • 管程锁定规则
    unlock 指令操作先于 lock 操作
  • volatile 变量规则、
    写操作先于读操作
  • 线程启动规则
    start 最先发生
  • 线程终止规则
    终止检测最后发生
  • 线程中断规则
    interrupt() 先行发生于代码检测中断
  • 对象终结规则
    初始化先于 finalize
  • 传递性
    A 先于 B,B 先于 C

需要注意的一点是,时间先后顺序与先行发生顺序没有太大的关系,一切以先行发生的原则为准。

volatile

有了内存模型的概念,对 volatile 又有了几分理解
Java 虚拟机提供的最轻量的同步机制,当一个变量被定义为 volatile 时有一下两种语义

可见性

volatile 变量对所有线程是立即可见的,对 volatitle 变量所有写操作都能立即反映到其他线程中。但注意他并不是安全的,因为该操作不是原子性的。所谓原子性,就是该变量执行如运算等操作只有等它运算完写入主内存中,其他线程才能操作该变量。没错,保证原子性你可以理解为加个锁。

禁止指令重排序

指令重排序指操为了采用流水线机制加快指令的处理速度
重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。重排序的前提是需要满足以下条件

  1. 在单线程环境下不能改变程序运行的结果;
  2. 存在数据依赖关系的不允许重排序

普通变量仅仅保证在方法执行过程中所有依赖赋值结果的地方能获取正确结果,而不能保证赋值操作和代码执行顺序一样

如何实现

  • 在该变量的写操作之前编译器插入一个写屏障
  • 在该变量的读操作之前编译器插入一个读屏障

翻译到汇编代码层面 lock addl $0x0,(%esp) 就是将 ESP 寄存器的值加 0,使本 CPU 的 Cache 写入内存,该写入操作会引起别的 CPU 或者别的内核无效化其 Cache。通过该空操作让前面对 volatile 变量的修改对其他 CPU 立即可见。

重排序为了优化,至于禁止重排序,也是将该变量的值强制刷出缓存,将修改同步到主内存,意味着所有之前的操作已经完成,也就达到了无法跨越内存屏障的效果了。这里要仔细体会。

注意点

  • volatile 不保证原子性
  • 屏蔽指令重排序操作 JDK 1.5 中才被修复,此前不可完全避免重排序

总结

强烈推荐《深入理解 Java 虚拟机》,看目录你会发现那些曾几何时听说过晦涩难懂的名字都会在此书中讲到。

------------- The End -------------
「不为五斗米折腰------真香」
0%