zoukankan      html  css  js  c++  java
  • GoF23:单例模式(singleton)

    单例模式简介

    • 核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
    • 常见场景:
      1. Windows的任务管理器
      2. Windows回收站
      3. 项目中,读取配置文件的工具类
      4. 网站的计数器一般也会采用单例模式,可以保证同步
      5. 数据库连接池的设计一般也是单例模式
      6. 在Servlet编程中,每个Servlet也是单例的
      7. 在Spring中,每个Bean默认就是单例的

    常见五种单例模式的实现方式

    饿汉式

    // 饿汉式
    public class Hungry {
        // 1.私有化构造器
        private Hungry() {
    
        }
    
        // 2.类初始化的时候,立即加载该对象
        private static Hungry instance = new Hungry();
    
        // 3.提供获取该对象的方法,没有synchronized,效率高
        public static Hungry getInstance() {
            return instance;
        }
    }
    
    • 饿汉式是最简单的单例模式的写法,保证了线程的安全,执行效率高,但不能延时加载。
    • 但饿汉式也存在一些问题,比如,在单例中要创建大量的数据。
    public class Hungry {
        private byte[] data1 = new byte[1024];
        private byte[] data2 = new byte[1024];
        private byte[] data3 = new byte[1024];
        private byte[] data4 = new byte[1024];
        
        private Hungry() {
        }
    
        private final static Hungry hungry = new Hungry();
    
        public static Hungry getInstance() {
            return hungry;
        }
    }
    
    • 在Hungry类中,定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入到内存中,如果长时间没有使用getInstance()方法,不需要Hungry类的对象,就会对内存造成一种浪费。

    • 我们希望只有在使用getInstance()方法时,才会去初始化单例类,加载单例类中的数据。因此,有了第二种单例模式:懒汉式

    懒汉式

    // 懒汉式
    public class LazyMan {
        // 1.私有化构造器
        private LazyMan() {}
    
        // 2.类初始化的时候,不立即加载该对象
        private static LazyMan instance;
    
        // 3.提供获取该对象的方法,有synchronized,效率较低
        public static synchronized LazyMan getInstance() {
           if (instance == null) {
               instance = new LazyMan();
           }
           return instance;
        }
    }
    
    • 懒汉式,保证线程安全,可以延时加载,但调用效率不高(获取该实例的方法上加了synchronized)。

    DCL懒汉式

    public class LazyMan {
        private LazyMan() {
        }
    	// 类初始化的时候,不立即加载该对象
        private static LazyMan lazyMan;
    	// 双重检查
        public static LazyMan getInstance() {
            if (lazyMan == null) {
             	// 静态同步代码块,锁对象为本类的class属性
    			/*
    			加锁是为了确保第一个拿到锁对象的线程创建对象后,
    			第二个拿到锁对象的线程无需再创建新对象
    			*/
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    
    • Double Checked Lock 双重锁,优化了懒汉式同步慢的问题,由于JVM底层内部模型原因,偶尔会出现问题,不建议使用
    • DCL懒汉式的单例,保证了线程的安全性,又符合懒加载,只有在用到的时候,才回去初始化,调用效率也比较高,但是这种写法在极端情况下,还是可能会有一定的问题。

    • 因为 lazyMan = new LazyMan();不是原子操作,至少会经过以下三个步骤:

    1. 分配内存
    2. 执行构造方法
    3. 指向地址
    • 由于指令重排,导致A线程执行lazyMan = new LazyMan();时,可能先执行了第三步(还没有执行第二步),此时B线程又进来了,发现 lazyMan不为空,直接返回了lazyMan,并且后面使用了返回的 lazyMan,由于此时线程A还没有第二部,导致此时lazyMan还不完整,可能会有一些意象不到的错误,所以就有了下面一种单例模式。

    • 这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

    public class LazyMan {
        private LazyMan() {
        }
    	// 增加volatile关键字,避免指令的重排,保证它的原子性和一致性
        private volatile static LazyMan lazyMan;
    	// 双重检查
        public static LazyMan getInstance() {
            if (lazyMan == null) {
             	// 静态同步代码块,锁对象为本类的class属性
    			/*
    			加锁是为了确保第一个拿到锁对象的线程创建对象后,
    			第二个拿到锁对象的线程无需再创建新对象
    			*/
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    饿汉式改进(静态内部类式)

    • 还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。
    // 静态内部类实现
    public class Holder {
        // 1.私有化构造方法
        private Holder() {
        }
    
        // 2.私有静态内部类
        private static class InnerClass {
            private static final Holder instance = new Holder();
        }
    
        // 3.获取该对象的方法
        public static Holder getInstance() {
            return InnerClass.instance;
        }
    }
    

    枚举单例

    public enum EnumSingleton {
        INSTANCE;
        public EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    
    • 枚举类型是纯天然的单例模式
    • 线程安全,调用效率高,但不能延时加载
    • 枚举类型是目前最推荐的单例模式的写法,因为足够简单,不需要自己开发去保证线程的安全性,同时可以有效的防止反射来破化我们的单例模式
    • newInstance的源码中:

    • 如果我们使用反射创建枚举类型对象,就会直接抛出异常

    防止反射破坏单例模式

    // DCL懒汉式
    public class LazyMan {                              
        private LazyMan() {}
    
        // 2.类初始化时,不立即加载该对象
        // 增加volatile关键字,避免指令的重排,保证它的原子性和一致性
        private volatile static LazyMan instance;
    
        // 3.获取该对象的方法
        public static LazyMan getInstance() {
            if (instance == null) {
                // 静态同步代码块
                // 和其他线程竞争本类的锁
                synchronized (LazyMan.class) {
                    if (instance == null) {
                        instance = new LazyMan();
                    }
                }
            }
            return instance;
        }
    }
    
    class LazyManTest {
        public static void main(String[] args) throws Exception {
        	// 调用静态方法创建
            LazyMan instance1 = LazyMan.getInstance();
            // 使用反射方式创建
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            LazyMan instance2 = declaredConstructor.newInstance();
    
            System.out.println(instance1 == instance2); // false
        }
    }
    

    问题:当我们调用懒汉模式的静态方法创建了一个单例对象,又使用反射方式创建了一个对象,通过比较得出:两个对象的引用不是同一个引用,这就用反射破坏了我们的单例模式。

    解决方式:在私有构造器中进行一个判断,如果 lazyMan 不为空,说明 lazyMan 已经被创建过了,如果正常调用 getInstance 方法,是不会出现这种事情的,所以直接抛出异常

    public class LazyMan {
        private LazyMan() {
            synchronized (LazyMan.class) {
                if (lazyMan != null) {
                    throw new RuntimeException("不要试图用反射破坏单例模式");
                }
            }
        }
    
        private volatile static LazyMan lazyMan;
    
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    但是这种写法还是有问题:在上面我们先是正常调用了 getInstance 方法,创建了LazyMan对象,而后第二次用反射创建对象,这样私有构造函数里的判断就起到作用,如果我们两次都是用反射创建对象呢?

    问题:如果两次都用反射创建对象,那么还是会创建两个不同的对象,依旧破坏了单例模式。

    原因private volatile static LazyMan lazyMan;这条语句中的lazyMan引用没有被赋值对象的地址,也就是说lazyMan对象没有被构造方法创建,在内存中没有用getInstance方法去初始化lazyMan引用,此时的lazyMan引用一直为null,在私有构造器中的判断就不起作用了。

    解决方式:定义一个静态布尔类型的flag变量,初始值为false,私有构造函数里面做一个判断,当第一次创建对象时,flag == false(肯定的),就把 flag 置为 true;当第二次创建对象时,flag已经为ture,这就说明出现问题了,正常的调用是不会第二次跑到私有构造方法中的,所以抛出异常。

    public class LazyMan {
        private static boolean flag = false;
        private LazyMan() {
            synchronized (LazyMan.class) {
                if (flag == false) {
                    flag = true;
                } else {
                    throw new RuntimeException("不要试图用反射破坏单例模式");
                }
            }
        }
        private volatile static LazyMan lazyMan;
        public static LazyMan getInstance() {
            if (lazyMan == null) {
                synchronized (LazyMan.class) {
                    if (lazyMan == null) {
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    }
    

    ​ 看起来很美好,但是还是不能阻止反射破坏单例模式,因为可以用反射破坏flag的值。

    并没有一个很好的方案去避免反射破坏单例模式,但枚举类型是纯天然防止使用反射创建对象的,如果枚举去newInstance就直接抛出异常了。

  • 相关阅读:
    如何实现分页功能
    学习Python的心路历程
    Python基础---协程
    Python基础---线程
    Python基础---python中的进程操作
    Python基础---进程相关基础
    Python基础---并发编程(操作系统的发展史)
    Python基础---网络编程3
    Python基础---网络编程2
    Python基础---面向对象3
  • 原文地址:https://www.cnblogs.com/rainszj/p/12188914.html
Copyright © 2011-2022 走看看