Java 单例模式
1、懒汉(线程不安全)
public class Singleton { private static Singleton instance; private Singleton() {} // 私有构造函数 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 懒加载:是
- 线程安全:否
- 说明:多线程时禁止使用。
2、懒汉(线程安全)
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 懒加载:是
- 线程安全:是
- 说明:使用同步方法保证线程安全,但效率太低。在创建对象之后,不应该限制多线程读取 instance。不推荐用来解决线程安全问题。
3、懒汉(双重校验锁)
public class Singleton { private volatile static Singleton singleton; private Singleton() {} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { // 注意此处还得有次判空 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
- 懒加载:是
- 线程安全:是
- 说明:使用 volatile 修饰变量保证可见性。这个是第二种方式的升级版。既保证了效率,又保证了安全,代码相比之下较复杂。相比于第二种方式,它的思路是使得对象创建好后,直接返回,不再给线程加锁(通过第一次非空判断实现)。synchronized 代码块内部之所以需要加非空判断,是因为多线程竞争时,可能有多个线程被阻塞,此时只需要第一个进入 synchronized 代码块内部的线程完成对象创建,其他线程即可直接获取,不用重复创建。
4、饿汉
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
- 懒加载:否
- 线程安全:是
- 说明:在类加载的时候创建对象。
5、饿汉(变种)
public class Singleton { private static Singleton instance = null; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
- 懒加载:否
- 线程安全:是
- 说明:和上面差不多,都是在类加载的时候创建对象。
6、静态内部类
public class Singleton { // 静态内部类 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() {} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
- 懒加载:是
- 线程安全:是
- 说明:注意这种方式和上面两种方式的不同。方式 3、4 都没有懒加载效果。而这种方式
Singleton
类被装载了,instance
不会被立马初始化,因为SingletonHolder
类没有被主动使用,只有通过显式调用getInstance
方法时,才会显式装载 SingletonHolder 类,达到了懒加载的效果。
7、枚举
public enum Singleton { INSTANCE; public void whateverMethod() { } }
- 懒加载:是
- 线程安全:是
- 说明:使用枚举方式实现,这种方式是
Effective Java
作者Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。这是实现单例模式的最佳方法,不过这种实现方式还没有被广泛采用。
为何枚举方式是最好的单例实现方式?
前几种方式实现单例都有如下 3 个特点:
- 构造方法私有化
- 实例化的变量引用私有化
- 获取实例的方法共有
这类实现方式的问题就在第一点:私有化构造器并不保险
。因为它抵御不了反射攻击
。
以大家最为常用的
饿汉式
为例,下面使用反射创建多个实例
public class Singleton implements Serializable { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } public class Main { public static void main(String[] args) throws Exception { Singleton s = Singleton.getInstance(); // 拿到所有的构造函数,包括非 public 的 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); // 使用空构造函数 new 一个实例。即使它是 private 的 Singleton sReflection = constructor.newInstance(); System.out.println(s); //com.fsx.bean.Singleton@1f32e575 System.out.println(sReflection); //com.fsx.bean.Singleton@279f2327 System.out.println(s == sReflection); // false } }
运行输出:
com.fsx.bean.Singleton@1f32e575 com.fsx.bean.Singleton@279f2327 false
通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。怎么解决?其实Joshua Bloch
说了:可以在构造函数在被第二次调用的时候抛出异常。具体示例代码,可以参考枚举实现的源码。
再看看它的序列化、反序列时会不会有问题。如下:
注意:JDK 的序列化、反序列化底层并不是反射。
public class Main { public static void main(String[] args) throws Exception { Singleton s = Singleton.getInstance(); byte[] serialize = SerializationUtils.serialize(s); Object deserialize = SerializationUtils.deserialize(serialize); System.out.println(s); System.out.println(deserialize); System.out.println(s == deserialize); } }
运行结果:
com.fsx.bean.Singleton@452b3a41 com.fsx.bean.Singleton@6193b845 false
可以看出,序列化前后两个对象并不相等。所以它序列化也是不安全的。
那么枚举呢?
使用枚举实现单例极其的简单:
public enum EnumSingleton { INSTANCE; public void whateverMethod() { } }
首先看看是否防御反射攻击:
public class Main { public static void main(String[] args) throws Exception { EnumSingleton s = EnumSingleton.INSTANCE; // 拿到所有的构造函数,包括非 public 的 Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); // 使用空构造函数 new 一个实例。即使它是 private 的 EnumSingleton sReflection = constructor.newInstance(); System.out.println(s); System.out.println(sReflection); System.out.println(s == sReflection); } }
结果运行就报错:
Exception in thread "main" java.lang.NoSuchMethodException: com.fsx.bean.EnumSingleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.fsx.maintest.Main.main(Main.java:19)
这个看起来是因为没有空的构造函数导致的,还并不能说明防御了反射攻击。那它有什么构造函数呢,可以看它的父类 Enum 类:
// @since 1.5 它是所有 Enum 类的父类,是个抽象类 public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { // 这是它的唯一构造函数,接收两个参数 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } ... }
既然它有这个构造函数,那我们就先拿到这个构造函数再创建对象试试:
public class Main { public static void main(String[] args) throws Exception { EnumSingleton s = EnumSingleton.INSTANCE; // 拿到所有的构造函数,包括非 public 的 Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);// 拿到有参的构造器 constructor.setAccessible(true); // 使用空构造函数 new 一个实例。即使它是 private 的 System.out.println("拿到了构造器:" + constructor); EnumSingleton sReflection = constructor.newInstance("testInstance", 1); System.out.println(s); System.out.println(sReflection); System.out.println(s == sReflection); } }
运行打印:
拿到了构造器:private com.fsx.bean.EnumSingleton(java.lang.String,int) Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.fsx.maintest.Main.main(Main.java:22)
第一句输出了,表示我们是成功拿到了构造器Constructor
对象的,只是在执行newInstance
时候报错了。并且也提示报错在Constructor
的 417 行,看看Constructor
的源码处:
public final class Constructor<T> extends Executable { ... public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { ... if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ... } ... }
主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0
。说明:反射在通过 newInstance 创建对象时,会检查该类是否 ENUM 修饰,如果是则抛出异常,反射失败
,因此枚举类型对反射是绝对安全的。
那么,枚举对序列化、反序列化是否安全?
public class Main { public static void main(String[] args) { EnumSingleton s = EnumSingleton.INSTANCE; byte[] serialize = SerializationUtils.serialize(s); Object deserialize = SerializationUtils.deserialize(serialize); System.out.println(s == deserialize); //true } }
结果是:true
。因此:枚举类型对序列化、反序列也是安全的。
综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:
- 线程安全
- 反射安全
- 序列化 / 反序列化安全
- 写法简单
ref: 单例模式 | 菜鸟教程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
2022-10-26 IDEA 跳到定义或使用
2021-10-26 AutoLISP command 命令中的 _.line line -line 有什么区别