一、单例模式的定义与特点

定义:单例(singleton)指一个类只有一个实例,且该类能够自行创建这个实例的一种模式。

单例模式有如下特点:

  • 单例类只有一个实例对象
  • 该单例对象必须由单例类来创建
  • 单例类提供一个访问该单例的全局访问点

二、单例模式的优缺点

优点

  • 单例模式可以保证内存中有且只有一个实例,减少了内存的开销
  • 可以避免对资源的多重占用
  • 单例模式设置全局访问点,可以优化和共享资源的访问

缺点

  • 单例模式一般没有接口,扩展困难,如果要扩展,则除了修改原来的代码,没有第二种途径,违背了开闭原则
  • 在并发测试中,单例模式并不利于代码调试,在调试过程中,如果单例模式的代码没有执行完,也不能创建一个新的对象
  • 单例模式通常写在一个类中,如果功能设计不合理就很容易违背单一职责原则

三、单例模式的应用场景

  • 需要频繁的创建一些类,使用单例可以降低系统内存压力,减少GC
  • 某类只要求生成一个对象,比如一个公司只有一个CEO
  • 某些类创建实例时占用资源较多,或者实例化时间较长,且经常使用
  • 某类需要频繁的实例化,而且创建的对象又频繁被销毁的时候, 如多线程的线程池,数据库连接池
  • 频繁访问数据库或者文件的对象
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

四、单例模式的结构与实现

单例模式是设计模式中最简单的模式之一。

通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

1.单例模式的结构

单例模式的主要角色如下:

  • 单例类:包含一个实例且能自行创建实例
  • 访问类:使用单例的类

2.单例模式的实现

1. 饿汉式

该模式的特点是类一旦加载就创建了一个单例,保证在调用getInstance方法之前单例就已经存在了,饿汉模式是当类一旦加载该实例后就会创建一个静态对象供系统使用,以后不在改变,因此是线程安全的。

  1. public class Singleton{
  2. /**
  3. * 私有的静态实例
  4. */
  5. private static Singleton instance = new Singleton();
  6. private Singleton(){}
  7. /**
  8. * 共有的静态方法
  9. */
  10. public static Singleton getInstance(){
  11. return instance;
  12. }
  13. }

2.懒汉式

2.1 懒汉模式(线程不安全)

该模式的特点是在类加载的时候没有生成单例,但是只有当第一次调用getInstance方法是才会去创建这个单例。

  1. public class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. if(instance = null){
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

很明显,上述代码在多线程的情况下,在if(instance ==null)这里会产生线程不安全因素。因此有了懒汉模式的线程安全版本

2.2 懒汉模式(线程安全)

要懒汉模式线程安全,只需要在getInstance方法的时候加锁即可。

  1. public class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){}
  4. public static synchronized Singleton getInstance(){
  5. if(instance == null){
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

3.双重检查模式

3.1 volatile 关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程之间对这个变量进行操作的可见性,即一个线程修改了这个变量,那么对另外一个线程来说是立即可见的。
  • 禁止进行指令重排序(说实话我也没有太明白)
    • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
    • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

3.2 双重检查模式

由于懒汉模式的锁添加在了方法上,因此,锁的粒度比较大,因此改写代码,只把一些重要的代码锁起来

  1. public class Singleton{
  2. private volatile static Singleton instance;
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. try{
  6. // 模拟创建对象之前做的一些准备工作
  7. Thread.sleep(3000);
  8. synchronized(Singleton.class){
  9. instance = new Singleton();
  10. }
  11. }catch(InterruptedException e){
  12. e.printStackTrace();
  13. }
  14. return instance;
  15. }
  16. }

此方法使用synchronized同步代码块,只对实例化的关键对象进行了同步,从语句的结构上说,运行的效率确实得到了提升,但是如果在多线程环境下,还是无法解决线程安全问题,我们接下来推出最终解决方案(DCL,双重检查模式)

  1. /**
  2. * DCL双重检查模式
  3. */
  4. public class Singleton{
  5. private volatile static Singleton instance;
  6. private Singleton(){}
  7. public static Singleton getInstance(){
  8. try{
  9. if(instance == null){
  10. // 模拟创建对象之前做的一些准备工作
  11. Thread.sleep(3000);
  12. synchronized(Singleton.class){
  13. if(insatnce == null){
  14. instance = new Singleton();
  15. }
  16. }
  17. }
  18. }catch(InterruptedException e){
  19. e.printStackTrace();
  20. }
  21. return instance;
  22. }
  23. }

问题:DCL什么时候会失效?

4.静态内部类单例模式

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

  1. public class Singleton{
  2. private Singleton(){}
  3. private static Singleton getInstance(){
  4. return SingletonHolder.sInstance;
  5. }
  6. private static class SingletonHolder{
  7. private static final Singleton sInstance = new Singleton();
  8. }
  9. }

5.枚举单例

  1. public enum Singleton {
  2. INSTANCE;
  3. public void doSomeThing() {
  4. }
  5. }

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:

  1. private Object readResolve() throws ObjectStreamException{
  2. return singleton;
  3. }

6.使用容器实现单例模式

用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

  1. public class ContainerSingletonManager {
  2. private ContainerSingletonManager() {}
  3. private static Map<String , Object> map=new HashMap<>();
  4. public static void putInstance(String key, Object value){
  5. if (key!=null && !key.isEmpty() && value!=null){
  6. if (!map.containsKey(key)){
  7. map.put(key,value);
  8. }
  9. }
  10. }
  11. public static Object getInstance(String key){
  12. return map.get(key);
  13. }
  14. }

五、单例模式如何破坏

1. 通过反射破坏单例

此处案例用DCL

通过反射获取它的构造函数,然后无视它的构造器的私有性,最后通过构造函数来实例化对象

  1. public static void main(String[] args) throws Exception {
  2. LazySingletonSafe instance1 = LazySingletonSafe.getInstance();
  3. // 获取它的无惨构造函数
  4. Constructor<LazySingletonSafe> declaredConstructor = LazySingletonSafe.class.getDeclaredConstructor(null);
  5. declaredConstructor.setAccessible(true);
  6. LazySingletonSafe instance2 = declaredConstructor.newInstance();
  7. System.out.println(instance1);
  8. System.out.println(instance2);
  9. }

运行结果如下:

  1. com.liyuan.designMode.singleton.instance.DclSingleton@1b6d3586
  2. com.liyuan.designMode.singleton.instance.DclSingleton@4554617c

既然有通过反射可以来破坏单例,那么我们对DCL进行升级,修改构造函数,在构造函数上加锁

/**
 * DCL双重检查模式
 */
public class DclSingletonSuper{
    private volatile static DclSingletonPlus instance;
    private DclSingletonPlus(){
        synchronized (DclSingletonPlus.class){
            if(instance != null){
                throw new RuntimeException("不要试图使用反射破坏");
            }
        }
    }
    public static DclSingletonPlus getInstance(){
        try{
            if(instance == null){
                synchronized(DclSingletonPlus.class){
                    if(instance == null){
                        instance = new DclSingletonPlus();
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return instance;
    }
}

再次运行上一个破坏函数发现失效,运行结果如下:

...
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏
    at com.liyuan.designMode.singleton.demage.DclSingletonPlus.<init>(DclSingletonPlus.java:17)
    ... 5 more

2. 两个实例都通过反射来new

话不多说,直接上破坏代码

public static void main(String[] args) throws Exception{
    Constructor<DclSingletonPlus> declaredConstructor = DclSingletonPlus.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    DclSingletonPlus instance1 = declaredConstructor.newInstance();
    DclSingletonPlus instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

运行结果如下:

com.liyuan.designMode.singleton.demage.DclSingletonPlus@1b6d3586
com.liyuan.designMode.singleton.demage.DclSingletonPlus@4554617c

显然我们需要再一次改进了,这次引入红绿灯机制来反制破坏,直接上代码

public class DclSingletonSuperPlus {
    private static boolean anoBabe = false;

    private volatile static DclSingletonSuperPlus instance;
    private DclSingletonSuperPlus(){
        synchronized (DclSingletonSuperPlus.class){
            if(anoBabe == false){
                anoBabe = true;
            }else{
                throw new RuntimeException("不要试图使用反射破坏");
            }
        }
    }
    public static DclSingletonSuperPlus getInstance(){
        try{
            if(instance == null){
                synchronized(DclSingletonSuperPlus.class){
                    if(instance == null){
                        instance = new DclSingletonSuperPlus();
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return instance;
    }
}

再次运行上一个破坏函数发现失效,运行结果如下:

Caused by: java.lang.RuntimeException: 不要试图使用反射破坏
    at com.liyuan.designMode.singleton.demage.DclSingletonSuperPlus.<init>(DclSingletonSuperPlus.java:18)
    ... 5 more

3. 通过反编译获得了红绿灯变量anoBabe

话不多说直接上破坏代码

public static void main(String[] args) throws Exception{
    // 已经通过反编译获得了红绿灯变量anoBabe
    Field anoBabe = DclSingletonSuperPlus.class.getDeclaredField("anoBabe");
    anoBabe.setAccessible(true);
    Constructor<DclSingletonSuperPlus> declaredConstructor = DclSingletonSuperPlus.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    DclSingletonSuperPlus instance1 = declaredConstructor.newInstance();
    anoBabe.set(DclSingletonSuperPlus.class,false);
    DclSingletonSuperPlus instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

运行结果如下:

com.liyuan.designMode.singleton.demage.DclSingletonSuperPlus@74a14482
com.liyuan.designMode.singleton.demage.DclSingletonSuperPlus@1540e19d

克制上述破坏,可以采用枚举单例来应对

尝试用反射来破坏,代码如下:

public static void main(String[] args) throws Exception {
    Enum1Singleton instance1 = Enum1Singleton.INSTANCE;
    //EnumSingleton instance2 = EnumSingleton.INSTANCE;
    //通过反射创建instance2
    Constructor<Enum1Singleton> enumSingletonConstructor = Enum1Singleton.class.getDeclaredConstructor(null);
    enumSingletonConstructor.setAccessible(true);
    Enum1Singleton instance2 = enumSingletonConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

执行异常信息如下:

Exception in thread "main" java.lang.NoSuchMethodException: com.liyuan.designMode.singleton.demage.Enum1Singleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.liyuan.designMode.singleton.demage.DemageSuperPlus.main(Enum1Singleton.java:22)

经过一番分析,(其实就是百度的)枚举类的构造函数是有参构造函数,修改破坏代码如下:

public static void main(String[] args) throws Exception {
    Enum1Singleton instance1 = Enum1Singleton.INSTANCE;
    //EnumSingleton instance2 = EnumSingleton.INSTANCE;
    //通过反射创建instance2
    Constructor<Enum1Singleton> enumSingletonConstructor = Enum1Singleton.class.getDeclaredConstructor(String.class,int.class);
    enumSingletonConstructor.setAccessible(true);
    Enum1Singleton instance2 = enumSingletonConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

发现异常信息如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.liyuan.designMode.singleton.demage.DemageSuperPlus.main(Enum1Singleton.java:24)

总结:枚举类本身就是一个类,且枚举不能够被反射