总结:!!使用枚举来实现单例功能
单例的常见场景
- Windows的任务管理器
- Windows的回收站
- 项目中,读取配置文件的类,一般也只有一个对象,没必须要每次都去new对象读取
- 网站的计数器一般也会采用单例模式,可以保证同步
- 数据库连接池的设计一般也是单例模式
- 在Servlet编程中,每个Servlet也是单例的
- 在Spring中,每个Bean默认就是单例的
- ...
单例模式 - 各个模式(从简单到复杂)&逐层破坏单例的做法及防御方案
饿汉模式(浪费内存空间)
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
!! 此时感觉可以一直破坏下去,只能通过查看源码来找到防御方案
由源码可知,枚举类可以防止通过反射的方式获取单例对象,由此引申出枚举类单例(枚举本身就是个单例)
代码写法如下
/**
* 枚举在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,可以得知有一个空参构造方法
因此可以有以下正常反射获取枚举实例
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,获取以下代码
可以看到依然是一个空参的构造方法,被骗了,所以需要一个更专业的反编译工具【jad.exe】,执行jad -sjava EnumSingle.class,可以用源文件生成一个EnumSingle.java文件
可以看到,存在一个有参构造器,接下来尝试一下
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)
和源码的异常信息一致,搞定
不常用的单例写法(静态内部类,是不安全的)
/**
* 静态内部类 - 是不安全的
*/
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();
}
}