首页 > 其他分享 >单例陷阱——双重检查锁中的指令重排问题

单例陷阱——双重检查锁中的指令重排问题

时间:2022-08-31 15:35:25浏览次数:46  
标签:单例 class instance static 陷阱 重排 线程 null LazyMan

最近在学习并发方面的知识,发现双重检查锁使用不当也并非绝对安全,在这里分享一下。

单例回顾

首先我们回顾一下最简单的单例模式是怎样的?

/**
*单例模式一:懒汉式(线程安全)
*/
public class Singleton1 {
    private static Singleton1 singleton1;
    private Singleton1() {
    }
    public static Singleton1 getInstance() {
        if (singleton1 == null) {
            singleton1 = new Singleton1();
        }
        return singleton1;
    }
}

这是一个懒汉式的单例实现,众所周知,因为没有相应的锁机制,这个程序是线程不安全的,实现安全的最快捷的方式是添加 synchronized

 /**
 * 单例模式二:懒汉式(线程安全)
 */
public class Singleton2 {
    private static Singleton2 singleton2;
    private Singleton2() {
    }
    public static synchronized Singleton2 getInstance() {
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

使用synchronized之后,可以保证线程安全,但是synchronized将全部代码块锁住,这样会导致较大的性能开销,因此,人们想出了一个“聪明”的技巧:双重检查锁DCL(double checked locking)的机制实现单例。

双重检查锁

一个双重检查锁实现的单例如下所示:

 /**
 * 单例模式三:DCL(double checked locking)双重校验锁
 */
public class Singleton3 {
    private static Singleton3 singleton3;
    private Singleton3() {
    }
    public static Singleton3 getInstance() {
        if (singleton3 == null) {
            synchronized (Singleton3.class) {
                if (singleton3 == null) {
                    singleton3 = new Singleton3();//不是一个原子操作
                }
            }
        }
        return singleton3;
    }
}

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:

  1. 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
  2. 在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。

程序看起来很完美,但是这是一个不完备的优化,在线程执行到第9行代码读取到instance不为null时(第一个if),instance引用的对象有可能还没有完成初始化。

问题的根源

问题出现在创建对象的语句singleton3 = new Singleton3(); 上,在java中创建一个对象并非是一个原子操作,可以被分解成三行伪代码:

//1:分配对象的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);  
//3:设置instance指向刚分配的内存地址
instance = memory;     

上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器中),即编译器或处理器为提高性能改变代码执行顺序,这一部分的内容稍后会详细解释,重排序之后的伪代码是这样的:

//1:分配对象的内存空间
memory = allocate(); 
//3:设置instance指向刚分配的内存地址
instance = memory;
//2:初始化对象
ctorInstance(memory);

在单线程程序下,重排序不会对最终结果产生影响,但是并发的情况下,可能会导致某些线程访问到未初始化的变量。

模拟一个2个线程创建单例的场景,如下表:

时间线程A线程B
t1 A1:分配对象内存空间  
t2 A3:设置instance指向内存空间  
t3   B1:判断instance是否为空
t4   B2:由于instance不为null,线程B将访问instance引用的对象
t5 A2:初始化对象  
t6 A4:访问instance引用的对象  

按照这样的顺序执行,线程B将会获得一个未初始化的对象,并且自始至终,线程B无需获取锁!

指令重排序

前面我们已经分析到,导致问题的原因在于“指令重排序”,那么什么是“指令重排序”,它为什么在并发时会影响到程序处理结果? 首先我们看一下“顺序一致性内存模型”概念。

顺序一致性理论内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行。
  2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

实际JMM模型

但是,顺序一致性模型只是一个理想化了的模型,在实际的JMM实现中,为了尽量提高程序运行效率,和理想的顺序一致性内存模型有以下差异:

在顺序一致性模型中,所有操作完全按程序的顺序串行执行。在JMM中不保证单线程操作会按程序顺序执行(即指令重排序)。 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。 顺序一致性模型保证对所有的内存写操作都具有原子性,而JMM不保证对64位的long型和double型变量的读/写操作具有原子性(分为2个32位写操作进行,本文无关不细阐述)

指令重排序

指令重排序是指编译器或处理器为了优化性能而采取的一种手段,在不存在数据依赖性情况下(如写后读,读后写,写后写),调整代码执行顺序。 举个例子:

//A
double pi  = 3.14;
//B
double r   = 1.0;
 //C
double area = pi * r * r;

这段代码C依赖于A,B,但A,B没有依赖关系,所以代码可能有2种执行顺序:

  1. A->B->C
  2. B->A->C 但无论哪种最终结果都一致,这种满足单线程内无论如何重排序不改变最终结果的语义,被称作as-if-serial语义,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉: 单线程程序是按程序的顺序来执行的。

双重检查锁问题解决方案

回来看下我们出问题的双重检查锁程序,它是满足as-if-serial语义的吗?是的,单线程下它没有任何问题,但是在多线程下,会因为重排序出现问题。

解决方案就是大名鼎鼎的volatile关键字,对于volatile我们最深的印象是它保证了”可见性“,它的”可见性“是通过它的内存语义实现的:

  • 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存
  • 读volatile修饰的变量时,JMM会设置本地内存无效

重点:为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来防止重排序!

对之前代码加入volatile关键字,即可实现线程安全的单例模式。

 /**
 * 单例模式三:DCL(double checked locking)双重校验锁
 */
public class Singleton3 {
    private static volatile Singleton3 singleton3;
    private Singleton3() {
    }
    public static Singleton3 getInstance() {
        if (singleton3 == null) {
            synchronized (Singleton3.class) {
                if (singleton3 == null) {
                    singleton3 = new Singleton3();
                }
            }
        }
        return singleton3;
    }
}

但是这种方式亦可以被反射破解

class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "ok");
    }
    private volatile static LazyMan LazyMan;
    //双重检测锁模式的懒汉式单例DCL 懒汉式
    public static LazyMan getInstance() {
        if (LazyMan == null) {
            synchronized (LazyMan.class) {
                if (LazyMan == null) {
                    LazyMan = new LazyMan(); //不是一个原子性操作
                }
            }
        }
        return LazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance2 = declaredConstructor.newInstance();
     System.out.println(instance);
     System.out.println(instance2);
}
}

此时解决方法在类中加锁synchronized解决

class LazyMan {
    private LazyMan() {
        synchronized (LazyMan.class){
            if (LazyMan!=null){
                throw new RuntimeException("不要试图用反射破解");
            }
        }
    }
    private volatile static LazyMan LazyMan;
    //双重检测锁模式的懒汉式单例DCL 懒汉式
    public static LazyMan getInstance() {
        if (LazyMan == null) {
            synchronized (LazyMan.class) {
                if (LazyMan == null) {
                    LazyMan = new LazyMan(); //不是一个原子性操作
                }
            }
        }
        return LazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }
}

 

但此时若两种都使用直接创建对象就不行了这时候就往类中添加private static boolean flag 对象来解决

class LazyMan {
    private static boolean flag = false;
    private LazyMan() {
        synchronized (LazyMan.class){
            if (flag==false){
                flag = true;
            }else {
                throw new RuntimeException("不要试图用反射破解");
            }
        }
    }
    private volatile static LazyMan LazyMan;
    //双重检测锁模式的懒汉式单例DCL 懒汉式
    public static LazyMan getInstance() {
        if (LazyMan == null) {
            synchronized (LazyMan.class) {
                if (LazyMan == null) {
                    LazyMan = new LazyMan(); //不是一个原子性操作
                }
            }
        }
        return LazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
     LazyMan instance2 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(instance2); } }

但此种方法能被获取到declaredfield中的flag属性此时解决方法运用枚举类来

enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}class Test {

    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        EnumSingle instance1 = EnumSingle.INSTANCE;

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        // NoSuchMethodException:single.EnumSingle. <init>()
        System.out.println(instance1);
        System.out.println(instance2);
        }
}

 

标签:单例,class,instance,static,陷阱,重排,线程,null,LazyMan
From: https://www.cnblogs.com/shine-xn/p/16643270.html

相关文章

  • 重绘和回流(重排)
    回流(重排)当RenderTree中部分或者全部元素的尺寸、结构、布局等发生改变时,浏览器就会重新渲染部分或全部文档的过程称为回流。重绘由于节点(元素)的样式的改变......
  • 静态——字段与属性、方法与类、单例设计模式
    1、static关键字static关键字,用于修饰类,字段,属性,方法,构造方法等。被static修饰的类称之为“静态类”;被static修饰的成员称之为“静态成员”;被修饰过的成员分别称为:......
  • 单例模式
    单例模式是什么?为什么要使用单例模式单例模式,顾名思义就是在整个运行时域,一个类只有一个实例对象为什么要用单例呢。因为有的类的,比较庞大和复杂,如果频繁创建和销毁对象,......
  • mybatis_13_SqlSessionFactory的DCL单例模式
    SqlSessionFactory的DCL单例模式 publicclassSqlSessionFactorySingleton{privateSqlSessionFactorySingleton(){}/***volatile关键字在此......
  • JAVA进阶--static、工具类、单例、继承--2022年8月28日
    第一节 static静态关键字1、成员变量的分类和访问分别是什么样的?静态成员变量(有static修饰,属于类,加载一次,可以被共享访问)访问格式:类名.变量......
  • 量化交易陷阱和R语言改进股票配对交易策略分析中国股市投资组合
    全文链接:http://tecdat.cn/?p=22034 原文出处:拓端数据部落公众号计算能力的指数级增长,以及量化社区(日益增长的兴趣使量化基金成为投资者蜂拥而至的最热门领域。量化......
  • spring-boot-starter-data-jpa + SQLite简单例子(含全部代码)
    简介1.sqlite:SQLite是比Access更优秀的文件型数据库,支持复杂的SQL语句,支持索引、触发器,速度很快,开源等。2.jpa:SpringDataJPA是Spring基于ORM框架、JPA规范的......
  • 变参模板改进单例模式
    单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点泛型单例模式需要变参构造函数,构造函数的参数个数需要支持变化下面是不用变参模板,支持0~6个参数的单例模式......
  • JavaScript设计模式及代码实现——单例模式
    单例模式1定义保证一个类仅有一个实例,并提供一个访问它的全局访问点。2应用时机当一个类的实例被频繁使用,如果重复创建这个实例,会无端消耗资源。比如dialog弹......
  • 单例模式的介绍
     单例模式是设计模式中常用的设计模式之一,它提供一种创建对象的方式。这种模式仅涉及一个单一的类,该类负责创建自身的对象(下面称对象为实例),同时确保只有一个对象被创......