简介
熟悉Spring的我们都知道,Spring替我们管理Bean,默认的类型就是
Singleton
,也就是单例,即全局唯一,并且提供一个全局访问点。
那么,我们自己如何实现单例呢?这里需要把握住几个核心要点:
- 静态化实例对象
- 私有化构造方法
- 提供一个公共的访问入口
单例模式的应用场景:
- 资源共享场景:避免由于资源操作时导致性能损耗
- 控制资源场景,便于资源之间互相通信
实现
单例模式的实现方式主要有四种:
- 饿汉式
- 懒汉式
- 登记式
- 枚举式
上述四种都可以实现单例,最推荐的方式还是枚举,因为其线程安全,而且可以防止反序列化和反射导致破坏单例。
饿汉式
见名知意,即在JVM加载的时候就创建相应的实例对象,这种方式最简单,也没有并发问题和效率问题,但是存在内存浪费问题,容易产生垃圾对象。
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() { return INSTANCE; } }
|
测试:
@Test public void testSingleton1() { Singleton1 instance1 = Singleton1.getInstance(); Singleton1 instance2 = Singleton1.getInstance(); System.out.println(instance1 == instance2); }
|
懒汉式
即延迟创建,使用到时才创建。这种方式需要注意并发问题和指令重排序问题,一般通过双重检查锁和内存屏障来解决。
public class Singleton2 {
private static volatile Singleton2 INSTANCE;
private Singleton2() { }
public static Singleton2 getInstance() { if (INSTANCE == null) { synchronized (Singleton2.class) { if (INSTANCE == null) { INSTANCE = new Singleton2(); } } } return INSTANCE; } }
|
也可以不使用volatile
关键字,但是需要将创建实例的语句拆成两句,就像下面这样:
public static Singleton2 getInstance() { if (INSTANCE == null) { synchronized (Singleton2.class) { if (INSTANCE == null) { Singleton2 tmp = new Singleton2(); INSTANCE = tmp; } } } return INSTANCE; }
|
这样,不管针对创建实例的语句如何进行重排序,都可以保证INSTANCE=tmp
语句拿到的是完整的实例对象。
测试:
@Test public void testSingleton2() { Singleton2 instance1 = Singleton2.getInstance(); Singleton2 instance2 = Singleton2.getInstance(); System.out.println(instance1 == instance2); }
|
登记式(静态内部类)
这种方式利用了JVM的类加载机制来保证只有一个实例。外部类加载时并不需要立即加载内部类,实现了Lazy
Load 的效果。实现方式简单,推荐。
JVM
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。
public class Singleton3 {
private Singleton3() { }
private static class LazyHolder {
private static final Singleton3 INSTANCE = new Singleton3(); }
public static Singleton3 getInstance() { return LazyHolder.INSTANCE; } }
|
测试:
@Test public void testSingleton3() { Singleton3 instance1 = Singleton3.getInstance(); Singleton3 instance2 = Singleton3.getInstance(); System.out.println(instance1 == instance2); }
|
枚举式
枚举可以说是单例模式的最佳实践
,代码非常简洁,保证线程安全
,自动支持序列化机制
,而且可以防止反序列化
。
public enum Singleton4 {
INSTANCE;
}
|
测试:
@Test public void testSingleton4() { Singleton4 instance1 = Singleton4.INSTANCE; Singleton4 instance2 = Singleton4.INSTANCE; System.out.println(instance1 == instance2); }
|
单例?
思考下,上述方式就一定能够保证单例吗?其实除了枚举可以,其他方式都是不行的,我们可以使用反序列化对象或者反射的方式来破坏上述的单例方式。
反射破坏单例
以 懒汉式 单例为例,其余同理。
@Test public void destroySingleton() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Singleton2 instance1 = Singleton2.getInstance();
Constructor<Singleton2> constructor = Singleton2.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton2 instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); }
|
通过获取到构造器来实例化新对象,破坏了单例。
解决方案:通过在构造方法中设置标识,来标识是否调用过构造函数。
private static boolean isInit = false;
public static Singleton2 getInstance() { if (INSTANCE == null) { synchronized (Singleton2.class) { if (isInit) { throw new IllegalStateException("单例模式已经初始化过了"); } isInit = true; if (INSTANCE == null) { INSTANCE = new Singleton2(); } } } return INSTANCE; }
|
序列化破坏单例
需要让 Singleton2 类实现 序列化接口 Serializable
@Test public void destroySingleton2() throws IOException, ClassNotFoundException { Singleton2 instance1 = Singleton2.getInstance();
FileOutputStream fos = new FileOutputStream("Singleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance1); oos.flush(); oos.close();
FileInputStream fis = new FileInputStream("Singleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton2 instance2 = (Singleton2) ois.readObject();
System.out.println(instance1 == instance2); }
|
解决方案:在被序列化的类中添加readResolve方法。
参考:https://docs.oracle.com/javase/7/docs/platform/serialization/spec/input.html
public class Singleton2 implements Serializable {
@Serial private static final long serialVersionUID = 1L;
private static volatile Singleton2 INSTANCE;
private Singleton2() { }
public static Singleton2 getInstance() { }
@Serial private Object readResolve() { return INSTANCE; } }
|
枚举实现原理
枚举是 JDK5 中提供的一种语法糖。
语法糖是在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便开发者使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。
通过对 Singleton4
这个类进行反编译,我们可以得到:
❯ javap Singleton4.class Compiled from "Singleton4.java" public final class com.youyi.singleton.Singleton4 extends java.lang.Enum<com.youyi.singleton.Singleton4> { public static final com.youyi.singleton.Singleton4 INSTANCE; public static com.youyi.singleton.Singleton4[] values(); public static com.youyi.singleton.Singleton4 valueOf(java.lang.String); static {}; }
|
由反编译后的代码可知,INSTANCE 被声明为 static
的,虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确的加锁、同步。所以,枚举实现在实例化时是线程安全。
除此之外,Java 规范中规定,每一个枚举类型及其定义的枚举变量在 JVM
中都是唯一的,因此在枚举类型的序列化和反序列化上,Java
做了特殊的规定:在序列化的时候 Java 仅仅是将枚举对象的 name
属性输出到结果中,反序列化的时候则是通过 java.lang.Enum
的
valueOf()
方法来根据名字查找枚举对象,因此反序列化后的实例和之前被序列化的对象实例相同。
总结
饿汉式
简单实用,且线程安全。不要求懒加载的情况下,推荐使用
静态内部类
的方式实现了懒加载
,比饿汉式更节省内存空间
懒汉式
如果不加锁就无法保证线程安全,如果加锁就会影响效率,较为复杂,不推荐使用
枚举式
比较简洁,线程安全,且防止反序列化,设计上最为完美
,推荐使用
- https://www.cnblogs.com/shoufeng/p/10820964.html
- https://zhuanlan.zhihu.com/p/150004430
- https://juejin.cn/post/7140266949962891277
- https://java.jverson.com/design/1-singleton.html