美文网首页
浅谈单例模式

浅谈单例模式

作者: 稚友22 | 来源:发表于2022-05-27 14:56 被阅读0次

    单例模式

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

    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<DoubleCheck> 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<DoubleCheck> 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<DoubleCheck> 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<DoubleCheck> 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<DoubleCheck> 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<EnumSingle> 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<EnumSingle> 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

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

    相关文章

      网友评论

          本文标题:浅谈单例模式

          本文链接:https://www.haomeiwen.com/subject/szgmprtx.html