单例模式常用写法总结

Posted ledphz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式常用写法总结相关的知识,希望对你有一定的参考价值。

引言

《创建单例模式的x种方法》在网上已经烂大街了,但这么多方式,会加重我的记忆负担,所以还得做个比较,把知识点浓缩一下,最终列出了三个比较常见的方法(其实是两个,只有静态内部类和枚举没有隐患,双检锁是有隐患的,请看下文中的防反射攻击处理一节)

三种方法比较

技术图片

如果想要懒加载:

我更推荐静态内部类,因为细节少,写法简单,不容易错。

如果不想要懒加载:

推荐枚举,自带防反射攻击和防序列化攻击,写法简单。

双检锁写法

public class DOUBLE_CHECK_LOCK_TEST {
    private volatile static DOUBLE_CHECK_LOCK_TEST Instance = null;
    
    private DOUBLE_CHECK_LOCK_TEST(){}
    
    public static DOUBLE_CHECK_LOCK_TEST getInstance(){
        if(Instance == null){
            synchronized (DOUBLE_CHECK_LOCK_TEST.class){
                if(Instance == null){
                    Instance = new DOUBLE_CHECK_LOCK_TEST();
                }
            }
        }
        return Instance;
    }
}

两个关键点,一是volatile防止指令重排,二是synchronized给类上锁

为什么双检锁要加volatile

因为 Instance = new Point(200,1); 这句话不是原子指令,其实有三步:

1.分配对象内存;

2.调用构造器,执行初始化;

3.将对象引用赋值给变量。

其中2和3可能发生指令重排,在并发环境下,线程A可能会先执行1和3,这时恰好切换到线程B,这时线程B就会看到一个不为null,但是没有完成初始化的对象,此时线程B访问这个对象就会发生异常。(详细请看双重检查锁单例模式为什么要用volatile关键字?)

静态内部类写法

public class STATIC_TEST {
    private static class STATIC_HOLDER{
        private static final STATIC_TEST Instance = new STATIC_TEST();
    }
    STATIC_TEST(){}
    public static final STATIC_TEST getInstance(){
        return STATIC_HOLDER.Instance;
    }
}

枚举写法

enum  ENUM_TEST {
    SINGLETON;
    public void doSomething() {
        System.out.println("doSomething");
    }
}

防序列化攻击处理

在单例类中加上readResolve()方法。

private Object readResolve(){
        return Instance;
    }

防反射攻击处理

在构造函数中加入检查。如果发现已经创建过,则不再创建。以双检锁为例:

private DOUBLE_CHECK_LOCK_TEST(){
        if(Instance != null){
            System.out.println("发现反射攻击,不许创建新实例!");
            throw new IllegalStateException();
        }
    }

但是在《Java单例---反射攻击单例和解决方法》这篇文章中指出:

如果先通过正常的获取手段获取实例,再进行反射攻击获取实例,此时是能防得住反射攻击的。

但如果反过来,先进行反射攻击获取实例,再通过正常的获取手段获取实例,得到的两个结果不同。也就是说,对于双检锁来说,这种处理依旧不能防止反射攻击,网上的部分博客都是错的。

还有一种尝试解决这种反射攻击的是:在单例里面加标识属性,如果实例化之后,标识改变,在构造器里面判断标识改变就抛异常,和上面这种气势差不多,但是没用的,反射可以把构造器的权限放开,同样可以把属性的权限放开,并且修改属性值,所以这种方式也是不行的

但是这种处理在静态内部类中,却不会产生上面双检锁出现的"先进行反射攻击获取实例,再通过正常的获取手段获取实例,得到的两个结果不同"的情况。

STATIC_TEST(){
        if(STATIC_HOLDER.Instance != null){
            System.out.println("发现反射攻击,不许创建新实例!");
            throw new IllegalStateException();
        }
    }

具体原因我还没弄清楚,希望看到的朋友可以解答一下。

参考

双重检查锁单例模式为什么要用volatile关键字?

Java单例---反射攻击单例和解决方法

以上是关于单例模式常用写法总结的主要内容,如果未能解决你的问题,请参考以下文章

C++ 单例模式总结与剖析

C++ 单例模式总结与剖析

最简单的设计模式——单例模式的演进和推荐写法(Java 版)

再见面试官:单例模式有几种写法?

单例模式你应该知道的几种写法

性能比较好的单例写法