您当前的位置: 首页 >  Java
  • 0浏览

    0关注

    1477博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

java并发编程(4)--单例模式的安全问题 volatile

软件工程小施同学 发布时间:2021-02-07 17:09:37 ,浏览量:0

一、单例模式的安全问题 1. 传统
package thread;

/**
 * 单例设计模式的安全问题
 * 常⻅的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会 有线程安全问题。
 */
public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }

    public static SingletonDemo getInstance(){
        if (instance == null) {
            instance = new SingletonDemo();
        }

        return instance;
    }

    public static void main(String[] args) {

        //main线程操作
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

    }
}

对象只创建了一次,单线程没有问题

 

 

一般在单例模式下使用.getInstance()创建对象;

但并不是所有 有 私有构造方法,对外通过getInstance方法提供实例的情况就是单例模式。 注:

单例模式:一个类有且只有一个实例。 1,一个私有的构造器 2,一个私有的该类类型的变量 3,必须有一个共有的返回类型为该类类型的方法,用来返回这个唯一的变量

https://www.cnblogs.com/baxianhua/p/9341953.html

 

 

2. 改为多线程操作测试
package thread;

public class SingletonDemo2 {

    private static SingletonDemo2 instance = null;

    private SingletonDemo2() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }

    public static SingletonDemo2 getInstance(){

        if (instance == null) {
            instance = new SingletonDemo2();
        }

        return instance;
    }

    public static void main(String[] args) {

        //多线程操作
        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                SingletonDemo2.getInstance();

            }, String.valueOf(i)).start();

        }

    }

}

多线程下,实例化多少次不一定

 

3. 调整后,采⽤常⻅的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会 有线程安全问题。
package thread;

public class SingletonDemo3 {

    private static SingletonDemo3 instance = null;

    private SingletonDemo3() {
        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造⽅法执⾏了");
    }

    public static SingletonDemo3 getInstance(){

        if (instance == null) {
            // 加锁,只能有一个线程执行下面
            synchronized (SingletonDemo3.class){
                if (instance == null) {
                    instance = new SingletonDemo3();
                }
            }
        }

        return instance;

    }

    public static void main(String[] args) {

        //多线程操作
        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                SingletonDemo3.getInstance();

            },String.valueOf(i)).start();

        }

    }

}

 

但是,当前程序有bug。

这个漏洞⽐较tricky,很难捕捉,但是是存在的。

instance=new SingletonDemo();可以⼤致分为三步:

instance = new SingletonDemo();

public static thread.SingletonDemo getInstance();

        Code:

            0: getstatic #11      // Field instance:Lthread/SingletonDemo; 
            3: ifnonnull 37
            6: ldc #12            // class thread/SingletonDemo 
            8: dup  
            9: astore_0
            10: monitorenter
            11: getstatic #11     // Field instance:Lthread/SingletonDemo; 
            14: ifnonnull 27
            17: new #12           // class thread/SingletonDemo 步骤1 
            20: dup
            21: invokespecial #13 // Method "":()V 步骤2 
            24: putstatic #11     // Field instance:Lthread/SingletonDemo;步骤3
底层Java Native Interface中的C语⾔代码内容,开辟空间的步骤

memory = allocate(); //步骤1.分配对象内存空间 
instance(memory); //步骤2.初始化对象 
instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null

 

剖析:

在多线程的环境下,由于有指令重排序的存在,DCL(双端检锁)机制不⼀定线程安全,我们可以加⼊ volatile可以禁⽌指令重排。

解决方法,给变量增加volatile

 

原因在与某⼀个线程执⾏到第⼀次检测,读取到的instance不为null时,instance的引⽤对象可能没有完成初始化。

memory = allocate(); //步骤1. 分配对象内存空间
instance(memory);    //步骤2.初始化对象
instance = memory;   //步骤3.设置instance指向刚分配的内存地址,此时instance !=null

步骤2和步骤3不存在数据依赖关系,⽽且⽆论重排前还是重排后,程序的执⾏结果在单线程中并没有改变,因此这种重排优化是允许的。

memory = allocate(); //步骤1. 分配对象内存空间 
instance = memory;   //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成! 
instance(memory);    //步骤2.初始化对象

但是指令重排只会保证串⾏语义的执⾏⼀致性(单线程),并不关⼼多线程的语义⼀致性。

所以,当⼀条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

public static SingletonDemo getInstance(){ 

    if (instance == null) { 
        synchronized (SingletonDemo.class){ 
            if (instance == null) { 
                instance = new SingletonDemo(); //多线程情况下,可能发⽣指令重排 
            }

        }
    
    } 
    
    return instance;

}

 

如果发⽣指定重排,那么,

  • 1. 此时内存已经分配,那么 instance=memory 不为null。
  • 2. 碰巧,若遇到线程此时挂起,那么 instance(memory) 还未执⾏,对象还未初始化。
  • 3. 导致了 instance!=null ,所以两次判断都跳过,最后返回的 instance`没有任何内容,还没初始化。

解决的⽅法就是对 singletondemo 对象添加上 volatile 关键字,禁⽌指令重排。

private static volatile SingletonDemo singletonDemo=null;

 

 

 

 

 

 

 

关注
打赏
1665320866
查看更多评论
立即登录/注册

微信扫码登录

0.0447s