彻底构造一个安全的单例模式

版权声明:本文来自kid_2412的csdn博客,欢迎转载! https://blog.csdn.net/kid_2412/article/details/53404827

正常情况下构造单例模式主要分为两种,饿汉和懒汉,即懒加载(延迟加载)和直接加载。

饿汉方式如下:

public class Singleton{
    private static final Singleton INSTANCE=new Singleton();
    private Singleton(){}

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

饿汉的方式是线程安全的,因为在类加载的时候就产生了Singleton的实例对象INSTANCE,初始化的时候已经用私有构造函数防止了new操作。虽然是线程安全的,但是通过反射攻击是可以破坏私有构造函数的。对于如何反射攻击和如何防止反射攻击,说完了懒汉模式再看,因为懒汉模式一样容易出现反射攻击。

懒汉模式:
懒汉模式有多种,当然是线程安全的和非线程的安全写法,当然我们最好还是保证线程安全。
先说线程不安全的:

public class Singleton{
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(INSTANCE==null){ //这里是线程不安全的,多个线程同时调用,会创建多个INSTANCE
            INSTANCE=new Singleton();
        }
    }
}

接下来保证线程安全:

public class Singleton{
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public synchronized static Singleton getInstance(){ 
    //加了synchronized线程安全了,但是会同步整个方法里的所有代码,显然有点得不偿失
        if(INSTANCE==null){ 
            INSTANCE=new Singleton();
        }
    }
}

改进synchronized:

public class Singleton{
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){ 
        synchronized(INSTANCE){//显然这里判断实例null需要进行竞争,也是得不偿失
            if(INSTANCE==null){ 
                INSTANCE=new Singleton();
            }
        }
    }
}

再改进synchronized:

public class Singleton{
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){ 
        if(INSTANCE==null){//这种双重校验虽然避免了上面的问题,但是对于-server模式下,会产生指令重排,会导致双重判断失效,依然不同步,于是再改进
            synchronized(INSTANCE){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
    }
}

再再改进synchronized:

public class Singleton{
    private volatile static Singleton INSTANCE=null; //volatile保证了指令顺序,不会重排
    private Singleton(){}
    public static Singleton getInstance(){  
        if(INSTANCE==null){ //虽然保证了指令重排,但是这么写法为何感觉这么操蛋?于是再改进
            synchronized(INSTANCE){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
    }
}

一个貌似很完善的单例,静态内部类:

public class Singleton{
    private Singleton(){}
    private static final class SingletonHandler{
        private static final Singleton INSTANCE=new Singleton();
    }
    public static Singleton getInstance(){  //借助了内部类天生的对线程安全的特性创建实例,不用同步锁了,也不用检查实例是否为null了
        return SingletonHandler.INSTANCE;
    }
}

为何说貌似很完善的单例,因为上面从饿汉到懒汉都可以用反射破坏私有构造函数封装性,然后用构造函数创建对象,如下代码:

public class SingletonTest{
    public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor constructor = Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object o = constructor.newInstance();
        System.out.println(Singleton.getInstance() == o);//这里会输出false,如下图
    }
}

这里写图片描述

这并不好,我们的单例不再是单例了!
避免被反射攻击的有两种方法,第一种:在私有构造函数里检查标记,像这样:

public class Singleton {
    private static boolean flag=false;
    private Singleton() {
        synchronized (Singleton.class){
            if(flag==false){//检查标记,如果为false,但是由于执行了改代码,构造函数肯定被调用了,于是让标记变为true。但是这个方法加锁了!
                flag=!flag;
            }else{//如果标记为true,证明被调用构造函数,抛出异常
                throw new RuntimeException("singleton is bad!");
            }
        }
    }

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

    private static final class UserUtilHandler {
        private static final Singleton INSTANCE = new Singleton();
    }
}

再改进一下,就彻底完美了,产生了第二种:使用枚举做单例,枚举天生线程安全,同时不会产生构造函数,代码如下:

//这个世界瞬间清净了许多!
public enum Singleton {
    INSTANCE;
    public void test(){
        System.out.print("i am a very very very safety singleton!");
    }

}

来看看枚举的暴力反射:

public class SingletonTest{
    public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor constructor = Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object o = constructor.newInstance();
        Singleton.INSTANCE.test();//调用实例方法
        System.out.println(Singleton.INSTANCE == o);
    }
}

这里写图片描述

可以看到,通过枚举构成的单例是最安全的。但是需要注意的是枚举构造单例在jdk1.5之前是不支持的!那么可以在jdk1.5之后使用枚举方式,1.5之前使用静态内部里+私有构造标记抛出异常的方式。

猜你喜欢

转载自blog.csdn.net/kid_2412/article/details/53404827