【设计模式】线程安全的单例模式

在工作中面试官会经常问你单例模式,而当你答出来的时候,又会接着让你写出线程安全的单例模式,下面我们就来学习一下,线程安全的单例模式的写法

首先我们知道单例模式有两种,分别是:

饿汉模式

public class SingletonExample2 {
    //私有化构造函数
    private SingletonExample2(){

    }
    //单例对象
    private static SingletonExample2 instance = new SingletonExample2();

    //静态工厂方法
    public static SingletonExample2 getInstance(){
       return instance;
    }
}

懒汉模式

public class SingletonExample1 {
    //私有化构造函数
    private SingletonExample1(){

    }
    //单例对象
    private static SingletonExample1 instance = null;

    //静态工厂方法
    public static SingletonExample1 getInstance(){
        if (instance == null){
            instance = new SingletonExample1();
        }
        return instance;
    }

从写法上我们可以看出,饿汉模式是线程安全的,但它的性能上会大大折扣。那么我们能否也让懒汉模式也变得线程安全呢?

答案是可以的


方法一.在方法上加上同步锁

直接在获取实例的方法上加上synchronized关键字

public class SingletonExample3 {
    //私有化构造函数
    private SingletonExample3(){

    }
    //单例对象
    private static SingletonExample3 instance = null;

    //静态工厂方法
    public synchronized static SingletonExample3 getInstance(){
        if (instance == null){
            instance = new SingletonExample3();
        }
        return instance;
    }
}

虽说该方法是线程安全的,但其性能也和饿汉模式差不多,在性能上会大大折扣,别急我们接着看

方法二.使用双重校验加同步锁机制
public class SingletonExample4 {
    //私有化构造函数
    private SingletonExample4(){

    }
    //指令重排问题:
    //1.分配内存空间
    //2.初始化对象
    //3.instance = memory设置instance指向刚分配的内存

    //单例对象
    private static SingletonExample4 instance = null;

    //静态工厂方法
    public static SingletonExample4 getInstance(){
        if (instance == null){  //双重检测机制
            synchronized (SingletonExample4.class) {    //同步锁
                if (instance == null){
                    instance = new SingletonExample4();
                }
            }
        }
        return instance;
    }
}

通过两次判断,确保创建的对象只能有一个,但这种方法还是存在线程安全的问题的。
在单线程的情况下,以上的代码没有丝毫问题,但在多线程的情况下,就会存在指令重排问题

那么什么是指令重排呢?

要知道指令重排我们首先需要知道正常的创建对象的顺序

  1. 分配内存空间
  2. 初始化对象
  3. 将对象的引用指向新分配的空间

在JMM中会存在一种优化方案,在程序执行时,系统为了系统的性能优化,可能会通过将一些程序的内部的顺序打乱。这就是指令重排的问题,例如创建对象时,可能会将步骤2和3的顺序打乱,如下

  1. 分配内存空间
  2. 将对象的引用指向新分配的空间
  3. 初始化对象

这样就会存在问题了,如代码:
在这里插入图片描述
当A、B线程达到以上位置时,发生指令重排,在A线程执行到指令2(将对象的引用指向新分配的空间)时,刚好CPU被B占用,这样B的对象指向了一个内存空间,但其对象并没有被实例化

那么怎么样才能解决以上问题呢?

我们可以给代码加上一个volatile关键字来防止指令重排

//单例对象
private static volatile SingletonExample4 instance = null;
方法三.使用枚举模式来创建对象

有了以上方法为何还会需要第三种方法呢?那是因为java中还有一种暴力的创建方法,反射,虽然不能通过关键字new来创建对象,但通过反射创建的对象,就不会是单例的了,那么有什么办法可以解决吗?答案是有的,就是使用枚举。

public class SingletonExample7 {
    private SingletonExample7(){}

    public static SingletonExample7 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;

        private SingletonExample7 singleton;

        //JVM保证这个方法绝对只调用一次
        Singleton(){
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}

好了,常见的几种线程安全的单例模式就介绍到这里了。如果有什么不足的地方,希望各位大佬能够指正。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值