1 简介
1.1 什么是单例模式?
Singleton is a creational design pattern, which ensures that only one object of its kind exists and provides a single point of access to it for any other code.
单例模式,一种创建型设计模式。它需要确保只有一个同类对象存在,并为任何其他代码提供对它的单一访问点。
不妨先看一段简单的代码示例:
public class SingletonDemo {
//私有的静态实例,提前准备好
private static SingletonDemo instance = new SingletonDemo();
//公开方法提供了私有实例的唯一获取点
public static SingletonDemo getSingleTonDemo(){
return instance;
}
//私有的构造方法确保了该对象实例不能在其他代码中new出来。
private SingletonDemo() {}
}
1.2 单例模式的特点
从单例模式的定义可以看出单例模式的两个重要特点:1. 有且仅有一个的唯一实例;2. 对外的公开访问方法。
结合定义和代码实现,可以看出代码实现其实是紧扣定义的:
- 私有的构造方法,确保了该类不能被其他代码通过构造方法制造出来,从而产生多个实例。
- 私有的静态实例,确保了实例的唯一性。
- 公开的访问方法,确保了其他系统可以使用唯一的实例。
1.3 单例模式的优缺点分析
让我们单从软件设计的角度分析下单例模式的优缺点:
-
优点
- 单例模式产生的唯一对象可以做到最大程度上的对象复用,从而减少内存的不必要浪费。
- 因为类控制了实例化过程,所以类本身可以更灵活地控制实例化过程。
-
缺点
- 单例模式违背了单一职责原则,类中耦合了实例化过程,这必然导致类难以测试、难以维护。
2 使用场景
2.1 类比真实世界
分析单例模式离不开类比现实世界的场景,任何模式的背后都是现实世界的场景的映射,所以从现实世界的场景出发更能加深记忆和理解。
设想一下,在我们的办公室,你需要打印一个文件,这个时候你是申请买一台打印机还是使用那台共用的打印机呢?答案显然是使用那台公用的打印机,因为不同于小物件,新买一台打印机的代价实在是太大了,我们使用公用的打印机可以节省的成本不是一点点。
这样的例子是非常多的,比如你不会造一个泳池而是去公用的泳池游泳,你不会重新聘请一个物业而是去物业中心去缴纳费用。
当面对这些庞大且需要公用的事物时,我们希望做到的就是在一定程度上确保单个唯一实例,如此也就可以最大可能的降低成本而不是铺张浪费。
2.2 程序中的单例模式
程序世界更是不乏这样的例子,比如JDBC连接池就是单例模式,Windows的任务管理就是单例模式,多线程编程的线程池也是单例模式。
3 代码实现
当掌握了单例模式的概念核心之后,显然单例模式有很多种代码实现。从简单的实现到考虑到线程安全和性能,实现方式的背后其实是更深层次的思考和逻辑。
3.1 饿汉式
之所以称为饿汉是因为实例直接是被new出来的,无论调用与否。就像一个饥饿的人,早早的把食物(实例)准备好了。当调用getInstance()方法的时候直接将已经准备好的实例提供即可。
public class HungrySingleton {
private HungrySingleton() {}
private static final HungrySingleton instance = new HungrySingleton();
public static HungrySingleton getInstance() {
return instance;
}
}
-
优点
- 写法简单
- 类加载过程中就已经实例化,避免了线程同步问题。
-
缺点
- 过早的实例化没有达到懒加载的效果,如果没有调用者,显然是浪费内存的。
3.2 懒汉式
之所以称为懒汉式,因为实例是在获取的时候才判断有没有,如果没有就new出来一个实例,如果有就返回已有的,像一个懒人等着别人催他一样。
public class LazySingleton {
private LazySingleton () {}
private static LazySingleton instance;
public static LazySingleton getInstance() {
if (instance == null) {
return new LazySingleton();
}
return instance;
}
}
-
优点
- 相比较饿汉式,显然懒汉式做到了懒加载,资源得到了节省。
-
缺点
- 但是多线程下,条件判断部分是线程不安全的。当存在多个线程就有可能产生多个实例。
3.3 线程安全的懒汉式
线程安全的懒汉式其实就是在获取实例的方法上加入synchronzed修饰符,以确保线程安全。
//同步方法,方法锁
public class SynchronizedLazySingleton {
private SynchronizedLazySingleton () {}
private static SynchronizedLazySingleton instance;
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
return new SynchronizedLazySingleton();
}
return instance;
}
}
}
//同步代码块,对象锁
public class SynchronziedBlockLazySingleton {
private SynchronziedBlockLazySingleton () {}
private static SynchronziedBlockLazySingleton instance;
public static SynchronziedBlockLazySingleton getInstance() {
if (instance == null) {
synchronized (SynchronziedBlockLazySingleton.getInstance()) {
return new SynchronziedBlockLazySingleton();
}
}
return instance;
}
}
-
优点
- 既做到了懒汉式的懒加载又做了线程安全。
-
缺点
- syncronized关键字本身对于线程安全的性能是不友好的。
3.4 双重检查
双重检查(Double check)是在对象锁的基础上再次升级,实例使用关键字volatile修饰确保唯一性。获取实例方法中双重判断是否已经存在。
public class DoubleCheckSingleton {
private DoubleCheckSingleton () {}
private volatile static DoubleCheckSingleton instance;
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.getInstance()) {
if (instance == null) {
return new DoubleCheckSingleton();
}
}
}
return instance;
}
}
-
优点
- 既做到了懒加载又做了线程安全并且效率得到了提高。
-
缺点
- 程序员记不住怎么写。
3.5 静态内部类
静态内部类在外部类装载过程中不会实例话,只有在调用getInstance方法时才会实例话,做到了懒加载。而类的静态属性只有在初始化的时候才会执行,从而确保了线程安全。
public class StaticInerClassSingleton {
private StaticInerClassSingleton() {}
private static class InnerSingleton {
private static final StaticInerClassSingleton INSTANCE = new StaticInerClassSingleton();
}
public static StaticInerClassSingleton getInstance() {
return InnerSingleton.INSTANCE;
}
}
-
优点
- 既做到了懒加载又做了线程安全还没有性能担忧。
-
缺点
- 程序员记不住怎么写。
3.6 枚举类
枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。
具体分析参考:https://www.cnblogs.com/chiclee/p/9097772.html
public enum EnumSingleton {
INSTANCE;
}
-
优点
- 线程安全,懒加载,没有性能问题。
- 简洁。
-
缺点
- 程序员不知道为什么这样写。
4 总结
单例模式的目的是确保系统中的庞大公用对象只存在唯一的实例从而做到最大程度的复用以提升系统性能比。
本文只聚焦于单例模式本身的含义,所以上述的实现方式没有过多的解释,然而具体每一种单例模式的实现都可以深究。当然我们不能忽略实际编码过程中的性能问题安全问题以及简介性,为此我们分别使用了以下方式并不断优化:
- 私有构造器 + 静态实例
- 私有构造器 + 静态实例存在判断
- 私有构造器 + 静态实例存在判断 + synchronzied关键字
- 私有构造器 + 静态实例存在判断 + synchronzied关键字 + volatile关键字 + double check
- 私有构造器 + 静态内部类
- 枚举类型的高度封装,浑然天成
我们从非线程安全非懒加载到线程安全和懒加载以及性能提升直至最后的枚举类在所有基础上还能防止私有构造器攻击以及解决序列化后对象不相等的问题。
其实static、synchronzied、volatile关键字以及枚举类型源码、静态内部类、如何通过反射攻破私有构造器、序列化问题这些概念单独一个都够讨论半天的,所以篇幅受限我们还是聚焦在单例模式的概念本身,其余概念和问题我们可以自行补齐。