JMM——Java内存模型

JMM即Java内存模型(Java memory model),在JSR133里指出了JMM是用来定义一个 「一致的、跨平台」
的内存模型,是缓存一致性协议,用来定义数据读写的规则。

内存可见性

在Java中,不同线程拥有各自的私有 「工作内存」
,当线程需要读取或修改某个变量时,不能直接去操作 「主内存」
中的变量,而是需要将这个变量的读取到线程的 「工作内存」
「变量副本」
中,当该线程修改其变量副本的值后, 「其它线程并不能立刻读取到新值」
,需要将修改后的值 「刷新到主内存中」
,其它线程才能 「从主内存读取到修改后的值」

指令重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序使得代码在 「多线程」
执行时会出现一些问题。

其中最著名的案例便是在 「初始化单例」
时由于 「可见性」
「重排序」
导致的错误。

单例模式

public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

以上代码是经典的 「懒汉式」
单例实现,但在多线程的情况下,多个线程有可能会同时进入 if (singleton == null)
,从而执行了多次 singleton = new Singleton()
,从而破坏单例。

public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

以上代码在检测到 singleton
为null后,会在同步块中再次判断,自以为可以保证同一时间只有一个线程可以初始化单例,但仍然存在问题。原因就是Java中 singleton = new Singleton()
语句并不是一个 「原子指令」
,而是由三步组成:

  1. 为对象分配内存
  2. 初始化对象
  3. 将对象的内存地址赋给引用

但是当经过 「指令重排序」
后,会变成:

  1. 为对象分配内存
  2. 将对象的内存地址赋给引用(会使得singleton != null)
  3. 初始化对象

所以就存在一种情况,当线程A已经将内存地址赋给引用时,但 「实例对象并没有完全初始化」
,同时线程B判断 singleton
已经不为null,就会导致B线程 「访问到未初始化的变量」
从而产生错误。

public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

以上代码对 singleton
变量添加了 volatile
修饰,可以阻止 「局部指令重排序」

那么为什么volatile可以保证变量的可见性和阻止指令重排序?

volatile

  1. 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
  2. 规定当线程使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
  3. 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入 「内存屏障」
    「防止指令重排序」

  1. volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的 「实际对象内部数据」
    的内存可见性。关于引用变量类型详见:Java的数据类型。

  2. volilate只能解决共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B 「已经正在使用」
    值为0的变量,所以这时候 「可见性已经无法发挥作用」
    ,线程B将其修改为1,所以最后结果并不是2而是1。

感谢您阅读本文,您的关注与点赞是对我最大的支持!