15. 单例模式

作者: Next_吴思成 | 来源:发表于2018-07-02 00:54 被阅读2次

    定义

    单例模式(Singleton Pattern):保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    通俗理解

    每一家公司都会有打印机,公司里面每个员工打印东西的时候,都会使用这一台打印机🖨去进行打印,这样不仅大大节约了成本,而且还可以增加打印机的可维护性。试想一下,如果公司为每一个员工都配备了一个打印机,那么A4纸、墨水、电力都会是一个不小的支出,与此同时的是,如果某个人的打印机坏了,那么还得工作人员去修,那如果坏多几台,那么工作人员将会忙不过来。

    单例模式就是这样的一个模式,保证系统里面的某个类只能创建一个实例对象(一家公司只有一台打印机),每个调用方调用的对象都是同一个(所有员工用的都是这台打印机),这不仅仅节约了系统的资源(创建对象消耗内存),而且只要修改一处,就可以使得整个系统生效,不需要修改多处才能够达到目的。

    示例

    示例将使用打印机当示例。

    渣渣程序

    打印机

    public class Printer {
        public void printer() {
            System.out.println("打印机打印文档");
        }
    }
    

    员工,调用方

    public class Staff {
        public static void main(String[] args) {
            Printer printer = new Printer();
            printer.printer();
        }
    }
    

    优化

    使用单例模式进行优化。首先,不能让调用方通过new进行实例化,只能是服务提供者提供实例化的方法;然后,提供方实例化时候,无论实例化多少次,都返回同一个对象;最后,调用方只能调用提供方的实例化方法创建对象。

    类图

    这个方法只涉及到一个类,就不画类图了,直接上程序。

    程序

    关于单例模式的写法,就像是茴香豆有四种写法一样,单例模式也有n种写法。有懒汉式的,有饿汉式的,有线程安全的,有线程不安全的,有基于内部类的,有基于枚举类的... ...

    线程安全 饿汉式 不抗反射 不抗反序列化

    public class Singleton {
        private static final Singleton instance = new Singleton();
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    初始化的时候就创建了,消耗一定的资源,调用的时候直接使用,不需要判断,节约了时间,是一种空间换时间的做法。线程安全,因为类只会被装载一次。不抵抗反射和反序列化(实现了Serializable接口下),可以通过反射和反序列化创建多个实例。

    解决

    public class Singleton2 implements java.io.Serializable{
        private static final Singleton2 instance = new Singleton2();
        private Singleton2() {
            //解决反射创建实例的问题,反射调用后抛出异常
            if (instance != null) {
                throw new RuntimeException();
            }
        }
        public static Singleton2 getInstance() {
            return instance;
        }
        //解决反序列化创建实例的问题,从io流读取对象的时候会调用这个方法,readResolve创建的对象会直接替换io流读取的对象
        private Object readResolve() throws ObjectStreamException {
            return instance;
        }
    }
    

    非线程安全 懒汉式 不抗反射 不抗反序列化

    public class SingletonLazyLoad {
        private static SingletonLazyLoad instance;
        private SingletonLazyLoad() {
        }
        public static SingletonLazyLoad getInstance() {
            if(instance == null) {
                instance = new SingletonLazyLoad();
            }
            return instance;
        }
    }
    

    调用的时候才初始化,节约资源。非线程安全。不抗反射,不抗反序列化。抗反射,抗反序列化的解决办法见第一个。

    线程安全 懒汉式 不抗反射 不抗反序列化

    public class SingletonLazyLoadSyn {
        private static SingletonLazyLoadSyn instance;
        private SingletonLazyLoadSyn() {
        }
        public static synchronized SingletonLazyLoadSyn getInstance() {
            if(instance == null) {
                instance = new SingletonLazyLoadSyn();
            }
            return instance;
        }
    }
    

    在获取实例的方法上加上synchronized关键字,保证在多线程的情况下,只有一个线程能够访问到方法体,保证了线程的安全。没有进入方法的线程会不断尝试去获取锁,而且每个线程进入方法后,需要进行非空的判断才能获取到实例,造成性能消耗。

    非线程安全 双重检查锁定 允许重排序 不抗反射 不抗反序列化

    public class SingletonLazyDCL {
        private static SingletonLazyDCL instance = null;
        private SingletonLazyDCL() {}
        public static SingletonLazyDCL getInstance() {
            if(instance == null) {
                synchronized (SingletonLazyDCL.class) {
                    if(instance == null) {
                        instance = new SingletonLazyDCL();
                    }
                }
            }
            return instance;
        }
    }
    

    JVM的优化,写的程序不一定按照我们写的那样执行下去,一般的创建对象顺序是先分配内存,然后创建对象,最后将这个对象指向相关的内存地址。由于JVM的指令重排序,这个过程可能会变成分配内存,将对象指向相关的内存地址,然后初始化对象。那么如果线程A走完分配内存和将对象指向相关的内存地址,但是没有完成初始化对象,线程B进来了,啊哈,A已经创建了,然后线程B就会出现获取到空实例的情况。

    线程安全 双重检查锁定 不允许重排序 不抗反射 不抗反序列化

    public class SingletonLazyDCLVolatile {
        private volatile static SingletonLazyDCLVolatile instance = null;
        private SingletonLazyDCLVolatile() {}
        public static SingletonLazyDCLVolatile getInstance() {
            if(instance == null) {
                synchronized (SingletonLazyDCL.class) {
                    if(instance == null) {
                        instance = new SingletonLazyDCLVolatile();
                    }
                }
            }
            return instance;
        }
    }
    

    volatile会保证我们的程序运行按照指定的方式去执行,不会出现重排序的情况,要么是null,要么是对象,不会出现地址不为空但是地址所指的对象为null的情况,这在一定的程度上是可以创建一个单例。但是,通过反射,我们还可以在这一个看似完美的程序上,创建多个实例。

    静态内部类 线程安全 不抗反射 不抗反序列化

    public class SingletonLazyLoadInnerClass {
        private static class SingletonHolder {
            static final SingletonLazyLoadInnerClass instance = new SingletonLazyLoadInnerClass();
        }
        private SingletonLazyLoadInnerClass(){}
        public static SingletonLazyLoadInnerClass getInstance() {
            return SingletonHolder.instance;
        }
    }
    

    外部类无法访问内部类,只有被调用SingletonLazyLoadInnerClass. getInstance ()的时候才完成初始化,利用ClassLoader的加载机制来实现难加载,并保证构建单例的线程安全。

    枚举 线程安全 抗反射 抗反序列化

    public enum SingletonEnum {
        INSTANCE;
        public void read() {
            System.out.println("====read====");
        }
    }
    class Main {
        public static void main(String[] args) {
            SingletonEnum.INSTANCE.read();
        }
    }
    

    不可以通过反射创建多个实例,但是使用的是非懒加载,单例对象在枚举类加载的时候就被初始化,可以抵抗反序列化。

    通过反射创建对象

    public class Ref {
        public static void main(String[] args) {
            try {
                Class<Singleton> single1 = Singleton.class;
                Class<Singleton> single2 = Singleton.class;
                Constructor<Singleton> c1 = single1.getDeclaredConstructor(null);
                Constructor<Singleton> c2 = single2.getDeclaredConstructor(null);
                c1.setAccessible(true);
                c2.setAccessible(true);
                Singleton s1 = c1.newInstance();
                Singleton s2 = c2.newInstance();
                System.out.println(s1);
                System.out.println(s2);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    //com.wusicheng.e15_singleton_pattern.nevv.Singleton@19e1023e
    //com.wusicheng.e15_singleton_pattern.nevv.Singleton@7cef4e59
    

    通过反序列化创建对象

    public class Ser {
        public static void main(String[] args) {
            Singleton before = Singleton.getInstance();
            try{
                FileOutputStream fos = new FileOutputStream("singleton.out");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(before);
                
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.out"));
                Singleton after = (Singleton) ois.readObject();
                ois.close();
                
                System.out.println(before);
                System.out.println(after);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    //com.wusicheng.e15_singleton_pattern.nevv.Singleton@224aed64
    //com.wusicheng.e15_singleton_pattern.nevv.Singleton@4e04a765
    

    优点

    1. 提供方严格管控这个类,严格控制调用方的调用方式
    2. 只创建一个实例,节省了资源,提高系统性能

    缺点

    1. 没有抽象层,不利于扩展
    2. 职责过重,违背单一职责原则
    3. JVM的垃圾回收可能会把单例类回收了,重新调用会重新创建

    应用场景

    1. 只需要一个实例对象的,例如工具类,资源管理类
    2. 调用方只允许有一个接口进入的

    实际例子

    JDK的java.lang.System

    参考

    1. 漫画:什么是单例模式?(整合版)
    2. 漫画:如何写出更优雅的单例模式?
    3. 设计模式----单例模式详解
    4. Android设计模式之单例模式
    5. 为什么我墙裂建议大家使用枚举来实现单例。
    6. 你真的会写单例模式吗?
    7. 设计模式之单例模式

    https://www.jianshu.com/p/6ac2690ec1f4

    相关文章

      网友评论

        本文标题:15. 单例模式

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