设计模式简讲程序员

15. 单例模式

2018-07-02  本文已影响2人  Next_吴思成

定义

单例模式(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

上一篇 下一篇

猜你喜欢

热点阅读