单例模式(Singleton pattern)
1 概念
单例模式属于创建型设计模式,提供了一种访问类的最佳方式。
单例模式要求类自己创建本类对象,且只能创建一个对象;这个类提供访问类的唯一方式;
即单例类的要求为:
- 只能创建一个对象
- 自己创建本类实例
- 提供供其它类访问该类实例的方法
2 实现思路
-
私有化构造函数
-
将创建的对象保存下来
-
提供方法对象实例的方法
3 实现方式
3.1 懒汉式
3.1.1 实现
public class SingletonClass01 {
private static SingletonClass01 singleClass;
private SingletonClass01() {
}
public static SingletonClass01 getSingleClass() {
if (singleClass == null) {
singleClass = new SingletonClass01();
}
return singleClass;
}
}
3.1.2 分析
上述代码只有在使用到的时候才会实例化对象,不使用时不占用资源;但是这种实现方式是线程不安全的,如果多个方法同时到达 singleClass == null
语句,可能都得到true的结果,导致创建多个对象,违反了单例的概念。
所以为实现线程安全,对get方法进行加锁
public static synchronized SingletonClass01 getSingleClass() {
if (singleClass == null) {
singleClass = new SingletonClass01();
}
return singleClass;
}
3.2 饿汉式
3.2.1 实现
public class HungrySingle {
private static final HungrySingle single = new HungrySingle();
private HungrySingle() {
}
private static HungrySingle getInstance() {
return single;
}
}
3.2.2 分析
本实现方法在类加载阶段即创建类的实例,与懒汉式方式相反,提前创建了对象,如果该类未被使用,则造成资源浪费。但是也因为提前创建了实例,不会出现single为空的情况,所以是线程安全的。
3.3 双重校验锁
3.3.1 实现
public class DoubleCheckLock {
private static volatile DoubleCheckLock single;
private DoubleCheckLock() {
}
public static DoubleCheckLock getInstance() {
if (single == null) {
synchronized (DoubleCheckLock.class) {
if (single == null) {
single = new DoubleCheckLock();
}
}
}
return single;
}
}
3.3.2 分析
为什么在加锁之后需要在判空
如果加锁之后不判空,是把所有线程阻塞在 single = new DoubleCheckLock()
中,锁被释放后,下一个线程仍旧会创建对象,并没有达到预期效果只创建一个实例。
为什么要使用volatile关键字
single = new DoubleCheckLock()
这一段代码有三个步骤并非原子的
- 分配内存空间
- 初始化对象
- 将对象地址赋给single
JVM在多线程的情况下可能会对指令进行重排,先1在3后2,从而导致最终赋给single的对象的地址但·并没有初始化对象,volatile禁止指令重排序的特性可以解决这个问题。
3.4 静态内部类
3.4.1 实现
public class StaticInnerClass {
private StaticInnerClass() {
}
private static class InstanceHolder {
private static final StaticInnerClass single = new StaticInnerClass();
}
public static StaticInnerClass getInstance() {
return InstanceHolder.single;
}
}
3.4.2 分析
这种写法既可以是线程安全的且是延迟加载的。
当类加载时InstanceHolder并没有被加载进内存,而是在调用getInstance
方法时执行 InstanceHolder.single
才加载对single进行初始化;而初始化的过程JVM对此提供了线程安全的保护。
3.5 枚举
3.5.1 实现
public class Singleton {
private Singleton(){
}
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
3.5.2 分析
实现单例模式的最佳方式
利用枚举的特性来解决线程安全和单一实例的问题,可以防止反射和反序列化对单例的破坏。
反射不能创建类的实例的原因是在反射newInstance时会对类的类型进行判断,如果是enum,则不予创建对象。
4 使用场景
如果一个全局使用的类频繁地被创建与销毁可以考虑使用单例模式,如果想控制实例数目,节省系统资源可以考虑使用单例模式。
- 多线程池中的线程池
- 数据库的连接池
- 应用程序日志
- 全局唯一弹窗,如点击一个按钮弹窗提示信息,只要出现一个给用户看即可,重复弹窗过多也会造成资源浪费,同时可能导致原来的变量引用的地址被改变,之前的弹窗无法手动控制销毁的问题。
5 总结
-
单例模式提供了对唯一实例的受控访问,在内存中只存在一个实例可以节约系统资源,在特定条件下可以提高系统性能。
-
但是单例模式也有一定的缺点,它违背了单一职责原则,不易于扩展,不能保存彼此的状态。、
-
实现单例模式的方式主要有懒汉式、饿汉式、双重校验锁、静态内部类、枚举实现,他们在延迟性、线程安全上的特点、原理及实现方式有所不同,其中枚举被大多数书籍列为实现单例的最佳方式,因为它不仅可以满足上述要求,还可以防止反序列化、反射对单例的破坏。
In case I don’t see you, good afternoon, good evening and good night.