9.共享模型之内存
共享模型之内存
JMM
即 Java Memory Model
,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
问题
public class Test1 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
SleepUtil.sleep(1);
// 线程t不会如预想的停下来
run = false;
}
}
在睡眠1s后没有停止
分析
- 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
- 主线程在1秒后修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方案:关键字 volatile
volatile
(易变关键字) 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile
变量都是直接操作主存
有序性
指令重排序
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...;
j = ...;
也可以是
j = ...;
i = ...;
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性,volatile 修饰的变量,可以禁用指令重排
运用-balking 模式
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
实现一
public final class Singleton implements Serializable {
private Singleton() {}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
- 类通过
final
修饰:是为了防止子类继承,增加方法破坏类的单例性 - 如果该类实现了序列化接口,还需要增加
public Object readResolve()
方法来防止反序列化破坏单例 - 无参构造函数设为私有,是为了防止通过构造器实例化对象,从而破坏了类的单例性
private static final Singleton INSTANCE = new Singleton();
通过static关键字修饰后,能保证线程安全,因为静态成员变量初始化操作实在类加载的时候完成的,而类加载时由JVM保证线程安全- 通过
public static Singleton getInstance()
静态方法将实例返回可以提供更好的封装性,可以内部实现懒惰的初始化,还可以对创建单例对象时实现更多的初始化控制,还可以提供泛型的支持
实现二
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
虽然能保证线程安全,但是效率低,因为每次获取实例都需要加锁,而 synchronized 本身就是重量级锁
实现三:DCL
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现2, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
-
private static volatile Singleton INSTANCE = null;
这里加上 volatile 关键字是为了确保指令的有序性,如果不加,在 17行( INSTANCE = new Singleton(); )可能会发生指令重排序从而导致 先赋值后创建对象,而创建对象时真正的构造方法还未执行完,这是已经将引用赋值完成,其他线程来的时候返回的是一个未初始化完成的对象实例。 -
对比实现二,缩小了临界区代码块内容,减少了锁时间,提高了并发量
-
如果对个线程都执行到了
synchronized (Singleton.class)
这一行,虽然只有一个线程能继续执行,但是这个线程执行完成后,其他线程竞争到锁以后又会继续执行,如果不判空,那么就是再实例化一次
实现四(推荐)无锁懒汉模式
public final class Singleton {
private Singleton() { }
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}