9.共享模型之内存

共享模型之内存

JMMJava 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后没有停止

分析

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

  1. 主线程在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;
    }
}
0%