设计模式 - 单例模式

109 阅读8分钟

学习视频

总结:!!使用枚举来实现单例功能

单例的常见场景

  1. Windows的任务管理器
  2. Windows的回收站
  3. 项目中,读取配置文件的类,一般也只有一个对象,没必须要每次都去new对象读取
  4. 网站的计数器一般也会采用单例模式,可以保证同步
  5. 数据库连接池的设计一般也是单例模式
  6. 在Servlet编程中,每个Servlet也是单例的
  7. 在Spring中,每个Bean默认就是单例的
  8. ...

单例模式 - 各个模式(从简单到复杂)&逐层破坏单例的做法及防御方案

饿汉模式(浪费内存空间)

public class Hungry {
    private Hungry() { }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }

    // 不获取Hungry实例,浪费内存空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];

}

如何优化以上方案,只在调用时创建实例呢?=> 懒汉模式

懒汉模式(仅单线程)

public class Lazy {

    private Lazy() {
        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    private static Lazy LAZY;

    public static Lazy getInstance() {
        if (null == LAZY) {
            LAZY = new Lazy();
        }
        return LAZY;
    }

    // 以上方法 单线程是OK的,多线程就不行(每次的执行结果,线程启动数量不一样)
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }


}

当前懒汉模式的写法只支持单线程,如果是多线程并发场景就无法满足,可以通过添加Synchronized的方式实现多线程下的单例

懒汉模式(支持多线程并发场景)

public class Lazy1 {

    private Lazy1() {
        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    // private static Lazy1 lazy1; // 不使用该声明方式

    private volatile static Lazy1 lazy1; // 使用该声明方式,下方会说明

    // 双重检测锁模式的 懒汉式单例 也称DCL懒汉式单例
    public static Lazy1 getInstance() {
        if (null == lazy1) {
            synchronized (Lazy1.class) {
                if (null == lazy1) {
                    lazy1 = new Lazy1(); // 该方法存在问题,下方会说明
                }
            }
        }

        return lazy1;
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Lazy1.getInstance();
            }).start();
        }
    }
}

注意:此处也会产生一个问题

lazy1 = new Lazy1(); // 不是一个原子性操作

/**
 * 该操作会执行以下三个步骤
 * 1. 分配内存空间
 * 2. 执行构造方法,初始化对象
 * 3. 把这个对象指向这个空间
 * 
 * 此时可能会发生“指令重排”的问题,即
 * 1. 正常情况下指令顺序为 1 2 3
 * 2. 但也有可能的指令顺序为 1 3 2
 * 3. 假设此时有一个线程A通过1 3 2的指令顺序,获取到了该单例对象,这没有问题。但此时接着线程B也同时获取,那么此时 `if (null == lazy1)` 会判定失败,进而直接`return lazy1`,此时对象是没有构造完成的,线程B会发生NullPointerException
 **/

解决方案 => 添加volatile

private static Lazy1 lazy1;改为private volatile static Lazy1 lazy1

接下来是对当前懒汉模式的破坏单例写法及防御方案

前提:已知当获取多个单例对象时,其每个对象指向的地址一定是相同的。

第一阶段的单例破坏:可以通过反射的方式,破坏单例,获取到多个不同地址的单例对象(即private在反射面前是泡沫)

前提代码如下:

public class Lazy2 {

    private Lazy2() {
        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    private volatile static Lazy2 lazy2;

    // 双重检测锁模式的 懒汉式单例 也称DCL懒汉式单例
    public static Lazy2 getInstance() {
        if (null == lazy2) {
            synchronized (Lazy2.class) {
                if (null == lazy2) {
                    lazy2 = new Lazy2();
                }
            }
        }

        return lazy2;
    }

    public static void main(String[] args) {
        Lazy2 lazy2 = Lazy2.getInstance();
    }
}

破坏单例写法

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Lazy2 instance1 = Lazy2.getInstance();

    // 反射获取
    Constructor<Lazy2> declaredConstructor = Lazy2.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true); // 无视私有构造器
    Lazy2 instance2 = declaredConstructor.newInstance();

    System.err.println(instance1);
    System.err.println(instance2);
    System.err.println("======");
    System.err.println(instance1.hashCode());
    System.err.println(instance2.hashCode());
    System.err.println("======");
    System.err.println(instance1 == instance2);
}

执行结果如下

main is OK
main is OK
com.shinefriends.juc.design_patterns.single.Lazy2@27716f4
com.shinefriends.juc.design_patterns.single.Lazy2@8efb846
======
41359092
149928006
======
false

第一阶段的防御方案:在构造器中再加一层锁控制,即三重检测

private Lazy2() {
    synchronized (Lazy2.class) {
        // 三重检测,防止反射暴力获取单例,但只能阻止正常获取单例+反射获取单例,不能阻止仅通过反射获取单例
        if (null != lazy2) {
            throw new RuntimeException("不要试图通过反射获取单例,不允许破坏单例的异常");
        }
    }

    System.err.println(Thread.currentThread().getName() + " is OK");
}

执行结果如下

main is OK
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.shinefriends.juc.design_patterns.single.Lazy2.main(Lazy2.java:40)
Caused by: java.lang.RuntimeException: 不要试图通过反射获取单例,不允许破坏单例的异常
	at com.shinefriends.juc.design_patterns.single.Lazy2.<init>(Lazy2.java:12)
	... 5 more

至此第一阶段的防御方案完成,但还可以继续破坏单例。当前防御方案只支持正常获取单例+反射获取单例,不能阻止仅通过反射获取

第二阶段的单例破坏:完全通过反射来获取多个单例对象,依然能获取到不同地址的单例对象

前提代码如下:

public class Lazy3 {

    private Lazy3() {
        synchronized (Lazy3.class) {
            // 三重检测,防止反射暴力获取单例,但只能阻止正常获取单例+反射获取单例,不能阻止仅通过反射获取单例
            if (null != lazy3) {
                throw new RuntimeException("不要试图通过反射获取单例,不允许破坏单例的异常");
            }
        }

        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    private volatile static Lazy3 lazy3;

    // 双重检测锁模式的 懒汉式单例 也称DCL懒汉式单例
    public static Lazy3 getInstance() {
        if (null == lazy3) {
            synchronized (Lazy3.class) {
                if (null == lazy3) {
                    lazy3 = new Lazy3();
                }
            }
        }

        return lazy3;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Lazy3 instance1 = Lazy3.getInstance();

        // 反射获取
        Constructor<Lazy3> declaredConstructor = Lazy3.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true); // 无视私有构造器
        Lazy3 instance2 = declaredConstructor.newInstance();

        System.err.println(instance1);
        System.err.println(instance2);
        System.err.println("======");
        System.err.println(instance1.hashCode());
        System.err.println(instance2.hashCode());
        System.err.println("======");
        System.err.println(instance1 == instance2);
    }
}

破坏单例写法

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    // 完全通过反射获取
    Constructor<Lazy3> declaredConstructor = Lazy3.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true); // 无视私有构造器
    Lazy3 instance1 = declaredConstructor.newInstance();
    Lazy3 instance2 = declaredConstructor.newInstance();

    System.err.println(instance1);
    System.err.println(instance2);
    System.err.println("======");
    System.err.println(instance1.hashCode());
    System.err.println(instance2.hashCode());
    System.err.println("======");
    System.err.println(instance1 == instance2);
}

执行结果如下

main is OK
main is OK
com.shinefriends.juc.design_patterns.single.Lazy3@27716f4
com.shinefriends.juc.design_patterns.single.Lazy3@8efb846
======
41359092
149928006
======
false

第二阶段的防御方案:新增一个标识位

private static boolean kieran = false; // 标识位随意取名

private Lazy3() {
    synchronized (Lazy3.class) {
        if (!kieran) {
            kieran = true;
        } else {
            throw new RuntimeException("不要试图通过反射获取单例,不允许破坏单例的异常");
        }
    }

    System.err.println(Thread.currentThread().getName() + " is OK");
}

执行结果如下

main is OK
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.shinefriends.juc.design_patterns.single.Lazy3.main(Lazy3.java:42)
Caused by: java.lang.RuntimeException: 不要试图通过反射获取单例,不允许破坏单例的异常
	at com.shinefriends.juc.design_patterns.single.Lazy3.<init>(Lazy3.java:15)
	... 5 more

至此第二阶段的防御方案完成,但还可以继续破坏单例。当通过反编译获取到标识位时,更改布尔值依然可以获取到不同地址的单例对象

第三阶段的单例破坏:通过反编译,找到其标识位为"kieran",反射获取标识位,并设置布尔值为false

前提代码如下

public class Lazy4 {

    private static boolean kieran = false;

    private Lazy4() {
        synchronized (Lazy4.class) {
            if (!kieran) {
                kieran = true;
            } else {
                throw new RuntimeException("不要试图通过反射获取单例,不允许破坏单例的异常");
            }
        }

        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    private volatile static Lazy4 lazy4;

    // 双重检测锁模式的 懒汉式单例 也称DCL懒汉式单例
    public static Lazy4 getInstance() {
        if (null == lazy4) {
            synchronized (Lazy4.class) {
                if (null == lazy4) {
                    lazy4 = new Lazy4();
                }
            }
        }

        return lazy4;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 完全通过反射获取
        Constructor<Lazy4> declaredConstructor = Lazy4.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true); // 无视私有构造器
        Lazy4 instance1 = declaredConstructor.newInstance();
        Lazy4 instance2 = declaredConstructor.newInstance();

        System.err.println(instance1);
        System.err.println(instance2);
        System.err.println("======");
        System.err.println(instance1.hashCode());
        System.err.println(instance2.hashCode());
        System.err.println("======");
        System.err.println(instance1 == instance2);
    }
}

破坏单例写法

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
    // 获取标识位
    Field kieran = Lazy4.class.getDeclaredField("kieran");
    kieran.setAccessible(true);

    // 完全通过反射获取
    Constructor<Lazy4> declaredConstructor = Lazy4.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true); // 无视私有构造器
    Lazy4 instance1 = declaredConstructor.newInstance();

    // 设置标识位为false
    kieran.set(instance1, false);

    Lazy4 instance2 = declaredConstructor.newInstance();

    System.err.println(instance1);
    System.err.println(instance2);
    System.err.println("======");
    System.err.println(instance1.hashCode());
    System.err.println(instance2.hashCode());
    System.err.println("======");
    System.err.println(instance1 == instance2);
}

执行结果如下

main is OK
main is OK
com.shinefriends.juc.design_patterns.single.Lazy4@8efb846
com.shinefriends.juc.design_patterns.single.Lazy4@2a84aee7
======
149928006
713338599
======
false

!! 此时感觉可以一直破坏下去,只能通过查看源码来找到防御方案

image.png

由源码可知,枚举类可以防止通过反射的方式获取单例对象,由此引申出枚举类单例(枚举本身就是个单例)

代码写法如下

/**
 * 枚举在jdk1.5就有了
 * 枚举本身就是一个类 class
 * 枚举是一个单例
 * 单例源码说无法破坏枚举
 */
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;

        System.err.println(instance1);
        System.err.println(instance2);
        System.err.println("======");
        System.err.println(instance1.hashCode());
        System.err.println(instance2.hashCode());
        System.err.println("======");
        System.err.println(instance1 == instance2);

    }
}

执行结果如下

INSTANCE
INSTANCE
======
41359092
41359092
======
true

如何通过反射破坏枚举,才能得到源码中的异常信息?

通过IDEA查看EnumSingle.class,可以得知有一个空参构造方法

image.png

因此可以有以下正常反射获取枚举实例

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    EnumSingle instance1 = EnumSingle.INSTANCE;
    EnumSingle instance2 = EnumSingle.INSTANCE;

    System.err.println(instance1);
    System.err.println(instance2);
    System.err.println("======");
    System.err.println(instance1.hashCode());
    System.err.println(instance2.hashCode());
    System.err.println("======");
    System.err.println(instance1 == instance2);

    Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    EnumSingle instance3 = declaredConstructor.newInstance();

    System.err.println(instance3);

}

执行结果如下

INSTANCE
INSTANCE
======
41359092
41359092
======
true
Exception in thread "main" java.lang.NoSuchMethodException: com.shinefriends.juc.design_patterns.single.EnumSingle.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.shinefriends.juc.design_patterns.single.Test.main(EnumSingle.java:34)

这个异常信息,与源码中的异常信息不一致

此时只能通过反编译查看源码。首先来到当前class目录下,执行javap -p EnumSingle.class,获取以下代码

image.png

可以看到依然是一个空参的构造方法,被骗了,所以需要一个更专业的反编译工具【jad.exe】,执行jad -sjava EnumSingle.class,可以用源文件生成一个EnumSingle.java文件

image.png

可以看到,存在一个有参构造器,接下来尝试一下

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    EnumSingle instance1 = EnumSingle.INSTANCE;
    EnumSingle instance2 = EnumSingle.INSTANCE;

    System.err.println(instance1);
    System.err.println(instance2);
    System.err.println("======");
    System.err.println(instance1.hashCode());
    System.err.println(instance2.hashCode());
    System.err.println("======");
    System.err.println(instance1 == instance2);

    // 加上参数
    Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);
    EnumSingle instance3 = declaredConstructor.newInstance();

    System.err.println(instance3);

}

执行结果如下

INSTANCE
INSTANCE
======
41359092
41359092
======
true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.shinefriends.juc.design_patterns.single.Test.main(EnumSingle.java:36)

和源码的异常信息一致,搞定 image.png

不常用的单例写法(静态内部类,是不安全的)

/**
 * 静态内部类 - 是不安全的
 */
public class Holder {

    private Holder() {
        System.err.println(Thread.currentThread().getName() + " is OK");
    }

    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }

    public static void main(String[] args) {
        Holder.getInstance();
    }
}