• 设计模式——单例模式


    单例模式

    个人主页

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

    这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    注意:

    • 1、单例类只能有一个实例。
    • 2、单例类必须自己创建自己的唯一实例。
    • 3、单例类必须给所有其他对象提供这一实例。

    饿汉式单例

    ​ 由于饿汉式单例是在类加载的时候创建的实例,避免了线程安全问题,所以是线程安全的。

    ​ 但是由于饿汉式是在类加载的时候就初始化,所以浪费内存。

    /**
     * Hungry   饿汉式单例
     */
    public class Hungry {
    	//如果此时加入一个成员,那类加载的时候就初始化,会浪费内存
        private Hungry() {
    			/*单例模式构造器都是私有的*/
        }
    
        private final static Hungry HUNGRY = new Hungry();
    
        public static Hungry getInstance() {
            return HUNGRY;
        }
    }
    

    静态内部类

    package Design_Patterns.Single;
    
    //静态内部类实现单例
    public class Holder {
    
        private Holder() {
            //单例模式,都必须构造器私有
        }
    
        public static Holder getInstance() {
            return InnerClass.HOLDER;
        }
    	//一个静态的内部类
        public static class InnerClass {
            private static final Holder HOLDER = new Holder();
        }
    }
    

    懒汉式单例

    线程不安全

    /**
     * 懒汉式单例
     */
    public class Lazy {
    
        private Lazy() {
            System.out.println(Thread.currentThread().getName() + "ok");
        }
    
        private static Lazy LAZY;
    
        public static Lazy getInstance() {
            if (LAZY == null) {
                LAZY = new Lazy();  //不是一个原子性操作
            }
            return LAZY;    
        }
    }
    

    分析:假如在getInstance()方法中,判断LAZY为null后,CPU切换到另一个线程,再来判断又是null,CPU继续切换回刚开始那个线程,继续执行new对象操作,然后CPU切换回第二个线程,也会顺着继续执行new对象操作,此时的对象就不再是单个的对象,违反了单例模式。

    我们对可以做一个测试:通过输出得知调用了四次构造函数,已经破坏了单例模式

    public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    public void run() {
                        Lazy.getInstance();
                    }
                }.start();
            }
        }
    /**output
    Thread-1ok
    Thread-3ok
    Thread-0ok
    Thread-2ok
    */
    

    线程安全

    为了解决线程安全问题,我们采用双重检验锁(DCL,即 double-checked locking)

    /**
     * 懒汉式单例
     */
    public class Lazy {
    
        private Lazy() {
            synchronized(Lazy.class) {
                if(LAZY != null) {		//防止反射破坏
                    throw new RuntimeException("不要试图使用反射破坏单例");
                } else {
                    System.out.println(Thread.currentThread().getName() + "ok");
                }            
            } 
        }
    
        private volatile static Lazy LAZY;
    
        //双重检测锁模式的懒汉式单例
        public static Lazy getInstance() {
            if (LAZY == null) {
                synchronized (Lazy.class) {
                    if (LAZY == null) {
                        LAZY = new Lazy();  //不是一个原子性操作
                    }
                }
            }
            return LAZY;    
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    public void run() {
                        Lazy.getInstance();
                    }
                }.start();
            }
        }
    }
    

    分析:

    为什么给LAZY对象加volatile关键字

    在Java中new一个对象并非一个原子操作,可分为三步:

    1. 分配内存空间
    2. 执行构造方法,初始化对象
    3. 把这个对象指向空间

    由于new对象并不是一个原子操作,所以可能发生指令重排,执行顺序可能是123,也可能是132,假如指令执行顺序变成了132:

    1. 假如A进程刚进来,先分配内存空间,再把对象指向这个空间
    2. 此时进来一个线程B,由于LAZY已经指向了一个空间,它会认为对象不为null,所以会直接返回
    3. 此时LAZY还未完成构造,空间是一片虚无,所以LAZY必须要避免指令重排,加volatile
    反射对单例的破坏

    Java的反射可以从class中反射出构造函数,从而达到创建对象的目的,也就破坏了单例的“只有一个实例”。

    public static void main(String[] args) throws Exception {
            Lazy lazy1 = Lazy.getInstance();
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);   //构造器是空参
            declaredConstructor.setAccessible(true);
            Lazy lazy2 = declaredConstructor.newInstance();
            System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
        }
    /** output
    705927765 ::: 366712642
    */ 
    

    可以看出两个对象并不是同一个对象,而是不同的两个对象,所以单例模式被破坏了。所以在构造函数里我们应该加上对对象的判断,如果LAZY已经不为空,就要抛出异常。

    more try

    当然除此之外,就算在构造器中加入了判断,也可以利用反射对单例造成破坏。判断是根据类中声明的对象是否为空来作为依据的,如果我们不调用getInstance()方法,而是直接利用反射构造出两个对象,即可避过这种检查,使LAZY一直等于null。

    public static void main(String[] args) throws Exception {
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);   //构造器是空参
            declaredConstructor.setAccessible(true);
            Lazy lazy1 = declaredConstructor.newInstance();
            Lazy lazy2 = declaredConstructor.newInstance();
            System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
        }
    /**output
    705927765 ::: 366712642
    */
    

    出现了这种情况,我已经可以解决。加入一个变量,这个变量的名字可以是加密过后的,在构造器中继续加入判断

    private static boolean flag = false;   //表示还未调用过构造器new对象
    
        private Lazy() {
            synchronized(Lazy.class) {
                if(flag == false) { //还未new过对象
                    flag = true;
                } else {
                    throw new RuntimeException("不要尝试使用反射破坏单例");
                }
            }
        }
    

    当然,这种也不是绝对安全的,如果利用反编译技术,可以得到flag这个变量(虽说已经加过密,但有加密也就有解密),那么flag依旧可以被反射出来,看下面示例:

    public static void main(String[] args) throws Exception {
    
            //对flag变量的反射
            Field flag = Lazy.class.getDeclaredField("flag");
            flag.setAccessible(true);
    		
        	//对构造器的反射
            Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);   //构造器是空参
            declaredConstructor.setAccessible(true);
            Lazy lazy1 = declaredConstructor.newInstance();
            flag.set(lazy1, false);				//将标志变量又变回false
            Lazy lazy2 = declaredConstructor.newInstance();
            System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
        }
    /** output
    366712642 ::: 1829164700
    */
    

    所以反射本就是一个bug,需要见招拆招,而不是一味的墨守成规。

    枚举

    ​ JDK1.5开始引入了枚举类型,它可以防止反射来破坏单例。

    ​ 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
    ​ 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
    ​ 不能通过 reflection attack 来调用私有构造方法。

    import java.lang.reflect.Constructor;
    
    public enum EnumSingle {
        INSTANCE;
    
        public EnumSingle getInstance() {
            return INSTANCE;
        }
    }
    
    class Test {
        public static void main(String[] args) throws Exception {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            EnumSingle instance2 = declaredConstructor.newInstance();
            System.out.println(instance1.hashCode());
            System.out.println(instance2.hashCode());
        }
    }
    /** output
    Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
    	at java.lang.Class.getConstructor0(Class.java:3082)
    	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    	at Test.main(EnumSingle.java:14)
    	*/
    

    抛出的异常说明enum中根本没有一个空参的构造方法,通过将class反编译为java文件,发现我们的类继承了枚举类,而构造器并非空参构造器,而是有参构造器,一个String和一个int

    //更改一下
    class Test {
        public static void main(String[] args) throws Exception {
            EnumSingle instance1 = EnumSingle.INSTANCE;
            Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
            declaredConstructor.setAccessible(true);
            EnumSingle instance2 = declaredConstructor.newInstance();
            System.out.println(instance1.hashCode());
            System.out.println(instance2.hashCode());
        }
    }
    /** output
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    	at Test.main(EnumSingle.java:16)
    */
    

    这样说明反射确实无法破解枚举的单例

    总结

    实现单例模式有四种方式,饿汉式、懒汉式、静态内部类、枚举。

    饿汉式:

    1. 线程安全
    2. 由于在类加载的时候初始化,浪费内存

    懒汉式:

    1. 要想线程安全得加锁,但加锁就会影响效率,但getInstance方法由于调用机会不多,所以影响不是很大
    2. 第一次调用才初始化,避免内存的浪费。

    静态内部类:

    1. 线程安全

    枚举:

    1. 线程安全
    2. 绝对防止多次实例化
    3. 自动支持序列化
  • 相关阅读:
    HTML_严格模式与混杂模式
    不要和一种编程语言厮守终生:为工作正确选择(转)
    iOS开发编码建议与编程经验(转)
    UTF-8 和 GBK 的 NSString 相互转化的方法
    UICollectionView 总结
    UIViewController的生命周期及iOS程序执行顺序
    objective-c 中随机数的用法
    clipsToBounds 与 masksToBounds 的区别与联系
    网络请求 代码 系统自带类源码
    iOS CGRectGetMaxX/Y 使用
  • 原文地址:https://www.cnblogs.com/vfdxvffd/p/13671697.html
Copyright © 2020-2023  润新知