彻底玩转单例模式
2021/5/11 10:59:04
本文主要是介绍彻底玩转单例模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
目录
- 1. 饿汉式
- 2. 懒汉式
- 3. 双重检测锁式
- 4. 静态内部类式
- --- 反射破坏单例模式,引入枚举单例
- 5. 枚举单例
- 饿汉式:类加载时初始化,不存在并发访问问题,会有资源浪费
- 懒汉式:延时加载,使用时才实例化对象,存在并发访问问题,资源利用率高
- 双重检测锁式 :利用sychronized关键字解决了懒汉式并发访问问题,同时为了解决指令重排问题使用了volatile关键字
- 静态内部类式:兼并并发高效调用和延迟加载的优势
- 枚举单例:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!但是不能延时加载。
1. 饿汉式
package 单例模式; //饿汉式 public class Hungry { private static Hungry hungry = new Hungry(); //构造器私有 private Hungry() { } public static Hungry getInstance() { return hungry; } }
优点:static变量会在类装载时初始化,不涉及多个线程访问该对象的问题,可以省略synchronized关键字
缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费!
2. 懒汉式
package 单例模式; //懒汉式 public class Lazy { private static Lazy lazy; private Lazy() { } public static Lazy getInstance() { if (lazy == null) lazy = new Lazy(); return lazy; } }
优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率
缺点:存在并发访问的问题,以下测试并发访问情况
package 单例模式; //懒汉式 public class Lazy { private static Lazy lazy; private Lazy() { System.out.println("创建示例"); } public static Lazy getInstance() { if (lazy == null) lazy = new Lazy(); return lazy; } public static void main(String[] args) { //10条线程并发访问下 for (int i = 0; i < 10; i++) { new Thread(() -> { Lazy.getInstance(); }).start(); } } }
根据结果,可以看到有5个线程打印了结果,也就说进行了5次初始化,这是非常大的漏洞,出现了并发访问的问题
3. 双重检测锁式
为了解决懒汉式并发访问的问题,加入了sychronized
关键字
package 单例模式; //双重检测锁式 public class DoubleLock { private static DoubleLock doubleLock; private DoubleLock() { System.out.println("创建示例"); } public static DoubleLock getInstance() { if (doubleLock == null) { synchronized (Lazy.class) { if (doubleLock == null) doubleLock = new DoubleLock(); } } return doubleLock; } public static void main(String[] args) { //10条线程并发访问下 for (int i = 0; i < 10; i++) { new Thread(() -> { DoubleLock.getInstance(); }).start(); } } }
根据打印结果,解决了并发访问的问题;但是这样仍然会存在问题,因为我们new
对象时并不是一个完整的原子性操作,而是分为以下三部:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
单个线程A执行的情况下可以123按顺序执行,也可能由于指令重排按132执行;但是如果线程A按132顺序执行到3时来了一个线程B,此时该对象已经指向了分配的空间,因此B判断对象不是null,就会直接返回对象,但其实对象并没有进行初始化,就造成了错误
因此指令重排也会导致错误,因此完整的双重检测锁式
还加入了Volatile
关键字来避免指令重排,完整代码如下:
package 单例模式; //双重检测锁式 public class DoubleLock { private volatile static DoubleLock doubleLock; private DoubleLock() { System.out.println("创建示例"); } public static DoubleLock getInstance() { if (doubleLock == null) { synchronized (Lazy.class) { if (doubleLock == null) doubleLock = new DoubleLock(); } } return doubleLock; } }
4. 静态内部类式
package 单例模式; public class InnerClass { private InnerClass() { } //静态内部类里面创建对象 public static class inner { private static final InnerClass innerClass = new InnerClass(); } public static InnerClass getInstance() { return inner.innerClass; } }
- 延时加载,只有真正调用getinstance(),才会加载静态内部类。
- 线程安全的,Instance是
static final
类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。 - 兼备了并发高效调用和延迟加载的优势
— 反射破坏单例模式,引入枚举单例
以下通过反射对双重检测锁式单例进行破坏
package 单例模式; import java.lang.reflect.Constructor; //双重检测锁式 public class DoubleLock { private volatile static DoubleLock doubleLock; private DoubleLock() { System.out.println("创建示例"); } public static DoubleLock getInstance() { if (doubleLock == null) { synchronized (Lazy.class) { if (doubleLock == null) doubleLock = new DoubleLock(); } } return doubleLock; } public static void main(String[] args) throws Exception { DoubleLock instance1 = doubleLock.getInstance(); Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null); constructor.setAccessible(true); DoubleLock instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }
根据结果,看到创建了两个实例,也就是单例模式被破坏,那么怎么解决呢?
可以在私有构造中加锁
package 单例模式; import java.lang.reflect.Constructor; //双重检测锁式 public class DoubleLock { private volatile static DoubleLock doubleLock; private DoubleLock() { synchronized (DoubleLock.class){ if(doubleLock!=null){ throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println("创建示例"); } public static DoubleLock getInstance() { if (doubleLock == null) { synchronized (Lazy.class) { if (doubleLock == null) doubleLock = new DoubleLock(); } } return doubleLock; } public static void main(String[] args) throws Exception { DoubleLock instance1 = doubleLock.getInstance(); Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null); constructor.setAccessible(true); DoubleLock instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }
根据结果,可以看到避免了单例模式的破坏?可是上述两个对象一个是通过单例获取,一个通过反射获取;
那如果两个对象都是通过反射获取呢?
public static void main(String[] args) throws Exception { Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null); constructor.setAccessible(true); DoubleLock instance1= constructor.newInstance(); DoubleLock instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2); }
根据结果,可以看到单例模式又被破坏了,创建了两个对象!这种情况如何解决呢?
可以通过红绿灯方法实现,定义一个标志位记录对象是否创建
package 单例模式; import java.lang.reflect.Constructor; //双重检测锁式 public class DoubleLock { private volatile static DoubleLock doubleLock; //标志位 private static boolean flag = false; private DoubleLock() { synchronized (DoubleLock.class) { if (flag == false) flag = true; else throw new RuntimeException("不要试图使用反射破坏异常"); } System.out.println("创建示例"); } public static DoubleLock getInstance() { if (doubleLock == null) { synchronized (Lazy.class) { if (doubleLock == null) doubleLock = new DoubleLock(); } } return doubleLock; } public static void main(String[] args) throws Exception { Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null); constructor.setAccessible(true); DoubleLock instance1 = constructor.newInstance(); DoubleLock instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }
可以看到我们通过设置标志位flag
再次解决了这个问题,但是一旦被获取了这个关键字,单例模式仍然可以通过反射被破解,如下所示
public static void main(String[] args) throws Exception { Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null); Field declaredField = DoubleLock.class.getDeclaredField("flag"); constructor.setAccessible(true); declaredField.setAccessible(true); DoubleLock instance1 = constructor.newInstance(); declaredField.set(instance1, false);//第一个对象创建完毕后将flag改为false DoubleLock instance2 = constructor.newInstance(); System.out.println(instance1); System.out.println(instance2); }
可以看到单例模式再次被破坏;因此为了让程序更加安全,通常对flag
关键字进行加密处理
那么到底如何完全的避免反射破坏单例模式呢?我们查看newInstance
的源码
可以看到,如果是枚举类型的话,就不能通过反射获取枚举;
因此引入了第5种单例模式
5. 枚举单例
-
优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
-
缺点:无延迟加载
package 单例模式; import java.lang.reflect.Constructor; //enum本质上就是一个Class类 public enum EnumSingle { INSTANCE; public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); } }
我们再次通过反射创建对象,根据结果报错没有EnumSingle的空构造方法,这不是我们希望看到的
我们对EnumSingle
的class文件进行反编译,可以看到明明有空构造方法
但是执行明明报错没有无参构造,我们使用更专业的反编译工具jad
对class文件再进行反编译
可以看到枚举类本质上就是继承了Enum
类,本身就是一个Class,而且没有无参构造,而是含两个参数的有参构造,我们修改代码在测试
public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class); declaredConstructor.setAccessible(true); EnumSingle instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); }
这才正确显示了报错的信息:无法反射地创建枚举对象
这篇关于彻底玩转单例模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南