乐趣区

关于运营:JMMJava内存模型

定义

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

内存可见性

在 Java 中,不同线程领有各自的公有 工作内存 ,当线程须要读取或批改某个变量时,不能间接去操作 主内存 中的变量,而是须要将这个变量读取到线程的 工作内存 变量正本 中,当该线程批改其变量正本的值后,其它线程并不能立即读取到新值 ,须要将批改后的值 刷新到主内存中 ,其它线程能力 从主内存读取到批改后的值

指令重排序!

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

其中最驰名的案例便是在 初始化单例时 因为 可见性 重排序 导致的谬误。

单例模式

案例 1

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(),从而毁坏单例。

案例 2

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 线程 拜访到未初始化的变量 从而产生谬误。

案例 3

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,所以最初后果是 1 而不是 2。

感谢您浏览本文,关注我的公众号“语冰 Yubing”可接管最新推送,外面也有我分享的一些优质资源。

退出移动版