前言
前几天经历了一次面试,原本的打算是检查自己的水平,好家伙,这一次面试直接给我干自闭了。😭😭😭
内容如题目所述: 单例模式
,如果你目前对于自己的 Java 有一点自信的话,那我建议你看看!
Joshua Bloch
大神说过的一句话: 实现单例模式的最佳方法是使用枚举
。
简介
单例模式(Singleton Pattern):确保只有一个类 有且只有
一个实例,并提供一个全局访问点。
在实际开发中,很多对象我们仅需要一个,例如:线程持( threadpool
)、缓存( cache
)、默认设置、注册表( registry
)、日志对象等等,而这个时候将他们设计为单例模式是最好的方式。
Java 中单例模式是一种很广泛使用的设计模式。
单例模式有很多好处:
- 避免对象的重复创建。
- 减少每次创建对象时的时间开销。
- 节约内存空间(例如 Spring 管理的无状态
bean
)。 - 避免多个实例之间操作导致的逻辑错误。
- 如果一个对象可能会贯穿整个应用,那么还会起到全局统一管理配置的作用。
方法
单例模式的写法非常多,但很多写法存在一些不足,下面以示例的方式加以指出。
懒汉(线程不安全)
1 | public class Singleton { |
这种写法 lazy loading
(懒加载)很明显,但是一看就知道,存在线程安全问题,所以这种写法是被禁止的。
懒汉(线程安全)
1 | public class Singleton { |
这种方式加了一个 synchronized
关键字来保证线程安全,但是效率太低了,毕竟 99.99%
的情况下是不需要同步的,有点用力过猛。极力不推荐使用!
饿汉
1 | public class Singleton { |
这种基于 classloader
方式b避免了多线程的同步问题,在类进行初始化的时候进行装载。这是目前最简单的实现方式。
饿汉(变种)
1 | public class Singleton { |
跟上面的类似,只是在类初始化是进行初始化实例。
静态内部类
1 | public class Singleton { |
刚分析了饿汉模式下的实现方式没有 lazy loading
的效果。
而这种方式类虽然被装载了,但是没有立刻进行初始化,因为静态内部类并没有被主动使用,只有显式调用 getInstance()
方法时,才会装载 SingletonHolder
类,显然他达到了 lazy loading
的效果。
双重校验锁(懒汉)
1 | public class Singleton { |
使用了 volatile
机制,保证了线程之间的可见性,这种方法俗称 双重检查锁定
。既保证了效率也保证了安全,不过代码显得比较复杂但是看起来比较高级。
枚举
1 | public class Singleton { |
这种方式是 Effective Java
的作者 Josh Bloch
提倡的方式,它不仅可以避免线程同步的问题,而且还可以防止序列化重新创建新的对象。所以目前这种写法是十分推荐的而且是最优的。
那么前几种方式实现单例模式都有以下三个特点:
- 构造方法私有化。
- 实例化的变量引用私有化。
- 获取变量的方法共有。
关于第一点
构造方法私有化
它并不保险,因为它无法抵挡反射攻击
,比如以下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public 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) {
Singleton s = new Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); // 拿到所有的构造函数,包括非 public 的
constructor.setAccessible(true);
Singleton sReflection = constructor.newInstance(); // 使用空构造函数 new 一个实例。即使它是 private 的
System.out.println(s); // cn.vgbhfive.beans.Singleton@1f32e575
System.out.println(sReflection); // cn.vgbhfive.beans.Singleton@279f2327
System.out.println(s == sReflection); // false
}
}通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。
再看看前几种方法的序列化和反序列化会不会出问题,如下:
1
2
3
4
5
6
7
8
9
10
11
12public class Main {
public static void main(String[] args) {
Singleton instance = new Singleton();
byte[] serialize = SerializationUtils.serialize(instance);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(instance); // cn.vgbhfive.beans.Singleton@1f32e575
System.out.println(deserialize); // cn.vgbhfive.beans.Singleton@279f2327
System.out.println(instance == deserialize); // false
}
}可以通过结果看出
序列化前后两个对象并不相等
,所以序列化也是不安全的。最后就来测试枚举在序列化和反序列化是否安全,如下:
1
2
3
4
5
6
7
8
9
10
11
12public class Main {
public static void main(String[] args) {
Singleton instance = EnumSingleton.INSTANCE;
byte[] serialize = SerializationUtils.serialize(instance);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(instance);
System.out.println(deserialize);
System.out.println(instance == deserialize); // true
}
}上面的结果已经很明显了,
枚举类型对于序列化和反序列化是安全的
。关于枚举在反射获取新实例方面的安全保障,主要在于以下几点方面:
- 无空的构造函数。
- 枚举类在创建对象时会检查该类是否有
ENUM
修饰。
具体内容可以去看看Enum
相关的源码。
- 综上,可以得出的结论:
枚举是实现单例模式的最佳实践
。优点如下:
- 反射安全。
- 序列化和反序列化安全。
- 写法简单。
- 其他方法都不足以说服不去使用枚举。
总结
单例模式作为设计模式中最简单、易理解的一种设计模式,在很多地方都有运用,也有极大的概率在面试中遇到。(比如我 )
Effective Java
这是一本很好的书,建议阅读,我买的书已经在路上了。
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!