浅谈单例模式

单例模式

饿汉模式:全局的单实例在类构建时构建

public class Hungary{
    
    private static final Hungary HUNGARY = new Hungary();
    private Hungary(){} 

    public static Hungary getInstance(){
        return HUNGARY;
    }
}

优点:

  • 饿汉式没有加任何的锁,因此执行效率比较高

缺点:

  • 饿汉式在一开始类加载的时候就实例化,无论使用与否,都会实例化,所以会占据空间,浪费内存,尤其是存在很多需要加载的资源情况下。

枚举模式:自带单例模式,枚举本质也是一个类,在jdk1.5之后就存在

public enum  EnumSingle {

    INSTANCE;
  
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

优点:代码实现简洁清晰。并且她还自动支持序列化机制,绝对防止多次实例化(防反射)。

懒汉模式:在加载类时不创建对象,在需要是在创建对象

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

优点:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。

缺点:线程不安全,在单一线程下没有问题,但是多线程就有问题了

public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    LazyMan instance = LazyMan.getInstance();
                    System.out.println(instance);
                }
            }).start();
        }
}

//=======结果=======
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@4bd8a307
com.zunhui.single.Hungary@36bdb2aa
com.zunhui.single.Hungary@721cca89
//==============

可以发现多次运行结果不一样,所以为了保证安全又诞生了双检索懒汉模式

双检索懒汉模式(DCL):通过加锁保证线程安全。

public class DoubleCheck{
    private DoubleCheck(){}
    //volatile 关键字作用可以是保证可见性或者禁止指令重排
    private volatile static DoubleCheck instance;
    
    public static DoubleCheck getInstance(){
        
        if(instance==null){
            synchronized(DoubleCheck.class){
                if(instance==null){ 
                    instance=new DoubleCheck();
                    /**
                    *new对象分三步
                    1.在内存开辟空间
                    2.调用构造器,初始化对象
                    3.将对象指向内存空间
                    但是,由于new对象不是原子性操作,所以可能存在指令重排执行顺序发生变化
                    a线程 123
                    b线程 132
                    //可能会导致空指针异常,所以需要给变量加volatile关键字
                    */
                }
            }
        } 
        return instance;
    }
}

优点:综合了懒汉式和饿汉式两者的优缺点整合而成,既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

思考?加锁就一定能保证线程安全嘛?

探究反射破坏单例模式

可以测试通过反射创建对象:

 public static void main(String[] args) throws Exception {
        DoubleCheck instance = DoubleCheck.getInstance();
     //反射创建对象  
     Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleCheck instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
//=========结果============
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4
//========================

我们可以通过测试看出,通过反射创建了一个新的对象,单例模式被破坏了。于是接着演变:

public class DoubleCheck {

    private DoubleCheck(){
        //在构造器中在加一层判断
        synchronized (DoubleCheck.class){
            if (instance!=null){
                throw new RuntimeException("不能通过反射创建对象~");
            }
        }
    }

    private volatile static DoubleCheck instance;

    public static DoubleCheck getInstance() {

        if (instance == null) {
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }
}

测试:

public static void main(String[] args) throws Exception {
    DoubleCheck instance = DoubleCheck.getInstance();
    
    Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~

[图片上传失败...(image-e0d83b-1653634504965)]

很明显我们加了三重验证防止反射创建对象,但是,这种情况是我们一开始就调用了getInstance()方法,执行了构造器中的同步代码,如果一开始就使用反射创建对象,那么依旧可以创建,测试一下:

public static void main(String[] args) throws Exception {
    Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance = constructor.newInstance();
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@45ee12a7
com.zunhui.single.DoubleCheck@330bedb4

为了防止这种情况,继续进行优化:

public class DoubleCheck {
    
   //定义一个标志位 可以是任意字符或者进行加密操作
   private static boolean bk = false;

    private DoubleCheck(){
        //同步之后将标志位设为ture 第二次调用构造器就报错,确保只创建一次
        synchronized (DoubleCheck.class){
            if (!bk){
                bk=true;
            }else {
                throw new RuntimeException("不能通过反射创建对象~");
            }
        }
    }

    private volatile static DoubleCheck instance;

    public static DoubleCheck getInstance() {

        if (instance == null) {
            synchronized (DoubleCheck.class) {
                if (instance == null) {
                    instance = new DoubleCheck();
                }
            }
        }
        return instance;
    }
}

继续测试:

 public static void main(String[] args) throws Exception {
        Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleCheck instance = constructor.newInstance();
        DoubleCheck instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
//结果报了异常
java.lang.RuntimeException: 不能通过反射创建对象~

说明可以通过加标志位的方式确保构造器只调用一次,只能创建一个对象。但是如果通过反编译等各种手段得到了标志位的话,依旧可以破坏单例模式,继续测试:

public static void main(String[] args) throws Exception {
    //通过反射获得标志位
    Field bk = DoubleCheck.class.getDeclaredField("bk");
    bk.setAccessible(true);

    Constructor constructor = DoubleCheck.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleCheck instance = constructor.newInstance();
    //在创建第一个对象后 恢复标志位
    bk.set(instance,false);
    DoubleCheck instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}
//结果
com.zunhui.single.DoubleCheck@330bedb4
com.zunhui.single.DoubleCheck@2503dbd3

结果我们又破坏了单例模式。

思考?那么,为什么通过反射就能破坏单例模式,就没有反射不能破坏的单例嘛?

我们通过查看constructor.newInstance();的源码:

//其中有这样一个异常,说反射不能创建enum对象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

那就说明反射不能创建枚举对象,在jdk中自己设置了不能通过反射创建枚举对象的机制。那我们测试一下:

编写一个枚举类:

public enum  EnumSingle {

    INSTANCE;
    
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

反编译枚举类java文件,查看.class文件:

image-20220527144151075.png

可以看到我们的枚举类本质也是一个类,继承了Enum父类,并且存在无参构造,那我们测试一下通过反射创建对象:

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance = EnumSingle.INSTANCE;
        Constructor constructor = EnumSingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        //NoSuchMethodException: com.zunhui.single.EnumSingle.
        EnumSingle instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }

结果:

image-20220527144409688.png

结果发现,报了一个不存在无参构造的异常,按理来说应该是报枚举不能被反射创建对象,那这是什么原因呢,我们接着探究,使用jad反编译工具将EnumSingle.class转为EnumSingle.java

public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/zunhui/single/EnumSingle, name);
    }
    //======================
    private EnumSingle(String s, int i)
    {
        super(s, i);
    }
    //======================
    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

通过查看源码,我们发现枚举类确实不存在无参构造,而是存在一个有参构造,两个参数分别是String和int,那我们接着修改测试代码:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    EnumSingle instance = EnumSingle.INSTANCE;
    //修改为获取有参构造
    Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
    constructor.setAccessible(true);
    //IllegalArgumentException: Cannot reflectively create enum objects
    EnumSingle instance1 = constructor.newInstance();
    System.out.println(instance);
    System.out.println(instance1);
}

结果:

image-20220527144907216.png

符合我们的预期,那就说明枚举类是一个特殊的类,通过有参构造实例化对象,且不能通过反射创建对象。

你可能感兴趣的:(浅谈单例模式)