15、彻底玩转单例模式
饿汉式 DCL懒汉式,深究!
饿汉式创建单例
饿汉式:顾名思义很饿:在类加载的时候,直接初始化对象
-
缺点:很浪费资源,因为对象没有被使用,但是已经初始化在内存了
-
比如:有下面这样的数组,会很浪费资源
package com.zxh.single; /** * 饿汉式:顾名思义很饿 * 1、在类加载的时候,直接初始化对象 * 2、很浪费资源,因为对象没有被使用,但是已经初始化在内存了 * 比如:有下面这样的数组,会很浪费资源 */ public class Hungry { private byte[] buffer1 = new byte[1024*1024]; private byte[] buffer2 = new byte[1024*1024]; private byte[] buffer3 = new byte[1024*1024]; private byte[] buffer4 = new byte[1024*1024]; // 构造器私有化 private Hungry(){ } // 直接初始化对象 private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } public static void main(String[] args) { Hungry hungry1 = Hungry.getInstance(); Hungry hungry2 = Hungry.getInstance(); System.out.println(hungry1); System.out.println(hungry2); } }
浪费资源所以就有了懒汉式创建单例模式
懒汉式创建单例
普通的懒汉式
package com.zxh.single; /** * 懒汉式创建单例模式 */ public class LazyMan { // 构造器私有化 private LazyMan(){ } private static LazyMan LAZY_MAN; public static LazyMan getInstance(){ if(LAZY_MAN == null){ // 如果为空,初始化对象 LAZY_MAN = new LazyMan(); } return LAZY_MAN; // 并返回 } public static void main(String[] args) { LazyMan lazyMan1 = LazyMan.getInstance(); LazyMan lazyMan2 = LazyMan.getInstance(); System.out.println(lazyMan1); System.out.println(lazyMan2); } }
多线程破坏普通的懒汉式
package com.zxh.single; /** * 懒汉式创建单例模式 */ public class LazyMan { // 构造器私有化 private LazyMan(){ System.out.println(Thread.currentThread().getName() + " OK"); } private static LazyMan LAZY_MAN; public static LazyMan getInstance(){ if(LAZY_MAN == null){ // 如果为空,初始化对象 LAZY_MAN = new LazyMan(); } return LAZY_MAN; // 并返回 } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getInstance(); }).start(); } } }
可以看到经过了4次构造方法,也就是创建了4个不同的对象。
那么如何解决多线程的并发问题?使用DCL (双重检测锁)懒汉式
DCL 懒汉式创建
-
DCL就是双重检测锁:解决并发问题
增加同步代码块:解决并发问题
-
修改
getInstance
这个方法
public static LazyMan getInstance(){ synchronized (LazyMan.class){ // 直接锁class模板 if(LAZY_MAN == null){ // 如果为空,初始化对象 LAZY_MAN = new LazyMan(); } } return LAZY_MAN; // 并返回 } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getInstance(); }).start(); } }
缺点:影响效率,因为每个线程都需要同步等待。
解决:在外面增加判断如果对象已经创建,那么直接返回
增加判断:解决效率问题
-
解决同步的效率问题
// DCL 双重检测锁 public static LazyMan getInstance(){ if (LAZY_MAN == null) { // 第一重检测 synchronized (LazyMan.class){ // 直接锁class模板,锁 if(LAZY_MAN == null){ // 如果为空,初始化对象,第二重检测 LAZY_MAN = new LazyMan(); } } } return LAZY_MAN; // 并返回 } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getInstance(); }).start(); } }
volatile:解决指令重排
存在问题:指令重排
// DCL 双重检测锁 public static LazyMan getInstance(){ if (LAZY_MAN == null) { // 第一重检测 synchronized (LazyMan.class){ // 直接锁class模板,锁 if(LAZY_MAN == null){ // 如果为空,初始化对象,第二重检测 /** * 但是真的安全吗?不安全 * 因为初始化对象不是原子操作,在极端情况下会进行指令重排 * 初始化对象的时候,不要以为只有一行代码,但是执行的时候会分成3步 * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向这个空间 * * 比如:我们希望执行的顺序为 123, * 假如A线程进入,经过指令重排执行顺序为 132,当执行到13,还没有执行2的时候,内存空间是分配了也指向了这个空间,但是对象是空的 * 此时B线程进入了,发现对象已经分配了空间,直接返回了,就会造成空指针 */ LAZY_MAN = new LazyMan(); } } } return LAZY_MAN; // 并返回 }
解决问题
/** * 懒汉式创建单例模式 */ public class LazyMan { // 构造器私有化 private LazyMan(){ System.out.println(Thread.currentThread().getName() + " OK"); } private volatile static LazyMan LAZY_MAN; // DCL 双重检测锁 public static LazyMan getInstance(){ if (LAZY_MAN == null) { // 第一重检测 synchronized (LazyMan.class){ // 直接锁class模板,锁 if(LAZY_MAN == null){ // 如果为空,初始化对象,第二重检测 LAZY_MAN = new LazyMan(); } } } return LAZY_MAN; // 并返回 } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getInstance(); }).start(); } } }
静态内部类创建
package com.zxh.single; // 静态内部类创建 public class Holder { private Holder(){ } public static InnerClass getInstance(){ return InnerClass.INNER_CLASS; } // 静态内部类,在程序加载时,并不会被初始化,所以不会浪费资源 private static class InnerClass{ private final static InnerClass INNER_CLASS = new InnerClass(); } public static void main(String[] args) { InnerClass innerClass1 = Holder.getInstance(); InnerClass innerClass2 = Holder.getInstance(); System.out.println(innerClass1); System.out.println(innerClass2); } }
反射破坏DCL和防止破坏
-
现在DCL 是目前我们认为最厉害的
-
但是在反射面前,一切都时候浮云
反射创建对象
1、创建两个对象:第一个为普通创建,第二个使用反射创建
-
会创建两个不同的对象
package com.zxh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * 懒汉式创建单例模式 */ public class LazyMan { // 构造器私有化 private LazyMan(){ } private volatile static LazyMan LAZY_MAN; // DCL 双重检测锁 public static LazyMan getInstance(){ if (LAZY_MAN == null) { // 第一重检测 synchronized (LazyMan.class){ // 直接锁class模板,锁 if(LAZY_MAN == null){ // 如果为空,初始化对象,第二重检测 LAZY_MAN = new LazyMan(); } } } return LAZY_MAN; // 并返回 } // NoSuchMethodException:没有这个方法 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { LazyMan lazyMan1 = LazyMan.getInstance(); Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null); LazyMan lazyMan2 = lazyManConstructor.newInstance(null); System.out.println(lazyMan1); System.out.println(lazyMan2); } }
解决:在构造器中在增加一重判断
// 构造器私有化 private LazyMan(){ synchronized (LazyMan.class){ if(LAZY_MAN != null) throw new RuntimeException("不要试图利用反射破坏单例"); } }
2、创建两个对象:两个对象都是用反射创建
// NoSuchMethodException:没有这个方法 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null); LazyMan lazyMan1 = lazyManConstructor.newInstance(null); LazyMan lazyMan2 = lazyManConstructor.newInstance(null); System.out.println(lazyMan1); System.out.println(lazyMan2); }
解决:增加一个标志变量,来判断是否是第一次创建对象
-
private static boolean flag = false:注意必须使用static修饰,才能是全局的变量
package com.zxh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * 懒汉式创建单例模式 */ public class LazyMan { private static boolean flag = false; // 增加一个标志,来判别是否已经创建了对象 // 构造器私有化 private LazyMan(){ synchronized (LazyMan.class){ if(flag == false){ // 第一次进入 flag = true; // 表示已将创建了对象 }else{ throw new RuntimeException("不要试图利用反射破坏单例"); } } } private volatile static LazyMan LAZY_MAN; // DCL 双重检测锁 public static LazyMan getInstance(){ if (LAZY_MAN == null) { // 第一重检测 synchronized (LazyMan.class){ // 直接锁class模板,锁 if(LAZY_MAN == null){ // 如果为空,初始化对象,第二重检测 LAZY_MAN = new LazyMan(); } } } return LAZY_MAN; // 并返回 } // NoSuchMethodException:没有这个方法 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null); LazyMan lazyMan1 = lazyManConstructor.newInstance(null); LazyMan lazyMan2 = lazyManConstructor.newInstance(null); System.out.println(lazyMan1); System.out.println(lazyMan2); } }
3、通过修改字段属性来破坏
// NoSuchMethodException:没有这个方法 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, ClassNotFoundException { Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null); LazyMan lazyMan1 = lazyManConstructor.newInstance(null); Field flag = LazyMan.class.getDeclaredField("flag"); flag.setAccessible(true); // 关闭安全监测锁,提高创建对象的效率 flag.set(lazyMan1, false); LazyMan lazyMan2 = lazyManConstructor.newInstance(null); System.out.println(lazyMan1); System.out.println(lazyMan2); }
哪怕这个标志的字段是加密的,也有可能会被反编译破解,从而获取字段信息。
所以说魔高一尺,道高一丈!
但是我们还可以利用枚举创建,因为它的底层就是不允许通过反射创建对象的
枚举创建单例和分析
枚举的单例创建
package com.zxh.single; public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } public static void main(String[] args) { EnumSingle instance1 = EnumSingle.INSTANCE; EnumSingle instance2 = EnumSingle.INSTANCE; System.out.println(instance1 == instance2); } }
源码分析反射的newInstance()方法
1、进入newInstance()方法
2、发现如果是枚举类,就抛出不能使用反射创建枚举类异常
package com.zxh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null); EnumSingle instance2 = constructor.newInstance(null); System.out.println(instance1 == instance2); } }
反射调用时,存在问题并解决问题
存在问题
发现抛出异常时没有这个方法,和想象中会抛出的异常不同,为什么?
是没有这个构造器吗?
1、通过编译生成的target包中对应的class文件查看
-
发现有这个构造器
2、通过反编译class文件查看,进入指定的目录在命令行输入:javap -p EnumSingle.class
-
发现也有这个构造器
难道是idea和jdk骗了我们?
3、使用专业的软件反编译
jad百度网盘下载 提取码: 9fpa
1)进入指定目录输入,jad -sjava EnumSingle.class
- 需要将该执行文件和class文件放在一起,并且会生成在同一目录下
2)编译得到的文件中可以发现没有空构造,但是有一个有参构造
解决问题
- 利用反射调用这个构造器
package com.zxh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class); EnumSingle instance2 = constructor.newInstance(null); System.out.println(instance1 == instance2); } }
总结
枚举类是创建单例模式最安全的,推荐使用!