1、三个重要特性

Java 内存模型(JMM)是围绕着如何在并发过程中处理原子性、可见性和有序性这三个特征建立起来的。下面将介绍这三个特性。

1.1、原子性(Atomicity)

原子性是指在一次或多次操作中,要么所有的操作全部执行成功,要么所有的操作都失败。比如

  • i=1 该操作是原子性的
    1. 将 i=1 写入工作内存
    2. 将 i=1 写入朱内存
  • j=i 非原子性的
    1. 从主内存中读取 i 的值,然后存入当前线程的工作内存中
    2. 在工作内存中修改 j 的值为 i,然后将 j 的值写入主内存
  • i++ 非原子性的
    1. 从主内存中读取 i 的值,然后存入当前线程的工作内存中
    2. 在执行线程工作内存中为 y 执行 +1 操作
    3. 将 i 的值写入主内存
  • i=i+1 和 i++ 相同,非原子性的

1.2、可见性(Visibility)

可见性是指当一个线程对共享变量进行了修改,其他线程可以立即看到修改后的新值。

1.3、有序性(Ordering)

有序性是指程序代码在执行过程中的先后顺序,由于 Java 在编译器以及运行期的优化,导致了代码的执行顺序未必就是程序员编写代码时的顺序。

2、JMM 如何保证三个特性的

2.1、原子性

由 Java 内存模型来直接保证的原子性操作包括 read、load、assign、use、store 和 write,也就是说 JMM 对基本数据类型的访问读写是具备原子性的。

如果想要使得某些代码片段具备原子性,JMM 提供了 lock 和 unlock 操作来满足这种需求。虚拟机并未把 lock 和 unlock 操作直接开发给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式得使用这两个操作,这个两个字节码指令反映到 Java 代码中就是同步块 synchronized 关键字。

另外,JUC 包中的 Lock 也可以满足这种情况

2.2、可见性

Java 提供了四种方式来保证可见性的

  1. final
    被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this 的引用传递出去,那在其他线程中就能看到 final 字段的值。
  2. volatile
    会将修改后的变量,刷新到主内存中,保证其他线程可见
  3. synchronized
    可以保证同一时刻只用一个线程获得锁,然后执行同步代码块,在释放锁之前,会将变量的修改刷新到主内存中
  4. 通过 JUC 提供的 Lock 也可以保证可见性
    Lock 的 lock 方法能够保证统一时刻只有一个线程获得锁,然后执行同步代码块,并且确保在 unlock 之前,会将变量的修改刷新到主内存中

2.3、顺序性

Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果一个线程中观察另一个线程,所有的操作都是无序的。前半句是指线程内表现为串行的语义(Within-Thread As-If-Serial Semantics),后半句是指指令重排序现象和工作内存与主内存同步延迟现象。

Java 提供了三保证种有序性的方式:

  1. 使用 volatile 关键字来保证有序性
  2. 使用 synchronized 关键字保证有序性
  3. 使用显示锁 Lock 来保证有序性

3、as-if-serial 语义

As-If-Serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程下程序的执行结果不会被改变。为了遵守 As-If-Serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行的结果。但是,如果操作之间不存在数据依赖关系,这些操作能被编译器和处理器重排序。

  1. int a = 1; // A
  2. int b = 2; // B
  3. int c = a * b; // C

上面的代码 A、C 之间和 B、C 之间存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面。但是 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。该程序的可能的执行顺序如下图所示:

三、并发编程的三个重要特性 - 图1

As-If-Serial 语义把单线程程序保护了起来,遵守 As-If-Serial 语义的编译器、runtime 和处理器共同为单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。As-If-Serial 语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。