单例模式-DCL懒汉式单例

单例模式即只有一个实例

基础铺垫

单例模式分为饿汉式与懒汉式,饿汉式为程序一启动就将实例创建好,而懒汉式为有需要的时候才创建实例

饿汉式:会提前创建好对象,但是会占用较多内存,如果这样的对象较多的时候会浪费内存,以空间换时间

public static class HungryMan {
    // 私有化无参构造器,使得外部不能通过new实例化对象
    private HungryMan(){}
    // 程序创建时就实例化对象
    private static HungryMan hungryMan = new HungryMan();
    // 通过对外方法返回对象
    public static HungryMan getInstance(){
        return hungryMan;
    }
}

懒汉式:需要使用的时候才会创建对象,不会浪费内存,但每次都会判断,是以时间换空间

public class LazyMan {
    private LazyMan(){}
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (null == lazyMan){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

正文开始

DCL懒汉式的出现是因为传统的懒汉式在多线程下并不安全,接下来,我们层层递进,从传统懒汉式优化至DCL懒汉式

改进1:在多线程下,判断lazyMan是否为空的时候,可能会两个线程判断的都空,所以会导致创建两次,能过加synchronized锁改进

if (null == testService){
    synchronized (TestService.class){
        if (null == testService) {
            testService = new TestService();
        }
    }
}

改进2:通过new关键字创建实例对象的个动作并不是原子性的,它可能存在以下问题

if (null == testService){
    synchronized (TestService.class){
        if (null == testService) {
            testService = new TestService(); // 不是原子性操作
            // 1. 分配内存空间
            // 2. 初始化对象
            // 3. 将对象指向空间
            // 由于指令重排,可能线程A执行的是123,线程B执行的是132
            // 此时线程B会认为testService != null,但其实它的空间是没有的
        }
    }
}

如果想要改进的话,需要在对象前加上volatile,禁止指令重排
关于volatile的特性,可以浏览博文的另一篇贴子:JUC-volatile关键字

private static volatile TestService testService;

至此,通过双重检测,基本可以保证懒汉式在多线程下正常运行,但是,这样的懒汉式仍然不够安全

改进3:我们可以通过反射获取到无参构造器并通过无参构造器创建对象

Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
LazyMan instance = LazyMan.getInstance();
System.out.println(instance == lazyMan1);
// 结果为false

这个时候改进很简单,只需要在无参构造器中加一个判断即可

private LazyMan(){
    synchronized (LazyMan.class) {
        if (null != lazyMan) {
            throw new RuntimeException("非法构造");
        }
    }
}

改进4:当所有对象都是通过反射创建的,能通过getInstance()方法时无参构造中的判断会失效

Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
LazyMan instance = declaredConstructors.newInstance();
System.out.println(instance == lazyMan1);

此时可以加一个标志位,判断是否创建过对象

private static boolean flag = false;
private LazyMan(){
    synchronized (LazyMan.class) {
        if (!flag){
            flag = true;
        }else {
            throw new RuntimeException("非法构造");
        }
    }
}

改进5:这个时候,如果将在创建对象前将标志位的值也进行变更,则又可以通过反向破坏单例模式

Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
Field flag1 = LazyMan.class.getDeclaredField("flag");
flag1.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
flag1.set(lazyMan1,false);
LazyMan instance = declaredConstructors.newInstance();
System.out.println(instance == lazyMan1);

最终解决方案:使用枚举,枚举的本质也是一个类,只不过继承了Enum类,它是自带单例模式的,而且在反射的源码中会检测类的类型,如果是枚举类型会抛出IllegalArgumentException异常

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

👍 欢迎前往博客主页查看更多内容

👍 如果觉得不错,期待您的点赞、收藏、评论、关注

👍 ​ 如有错误欢迎指正!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值