创建型——单例模式
目录
创建型——单例模式
单例模式是创建型设计模式中写法最最最多的一种(面试官也常问),提起单例模式,我们总会想到有饿汉式,懒汉式,枚举式等等实现方式。这个时候问题来了,我们往往对写法单一的知识能够清晰掌握,而对写法多样(虽然它本身并不难)的,在面试过程中,或者在使用过程中会手忙脚乱。归根结底还是没有熟悉每一种写法中对应比较适合的应用场景。本文尽量将每一种单例的写法与其对应适合的场景进行列举,让单例相关的知识更加深刻。
定义
- 保证系统中一个类仅有一个实例,并提供一个全局访问点
适用场景
- 确保任何情况下都绝对只有一个实例。
优点
- 减少内存开销,在内存里只有一个实例。
- 避免对资源的多重占用
- 设置全局访问点,严格控制访问
缺点
- 扩展困难
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化反序列化安全(了解)
序列化和反序列化安全指的是,将单例对象序列化之后,再反序列化之后,发现:是一个新对象。
解决方案:单例类里面写一个readResolve()方法,将单例对象返回就行了,具体原因是在ObjectStreamClass.java里面。
- 防御反射攻击(了解)
使用反射进行单例对象的创建会破坏单例模式。解决方法:在私有构造器里面进行逻辑判断即可。
code
饿汉式——必须掌握,因为它足够简单
优点和缺点
饿汉式的优点和缺点都围绕一点,那就是系统初始化就加载。它不管我们程序中是否使用了这个实例,哪怕在某个系统中只有1%的流量会触发使用了这个单例对象的业务,他也会一直占用着这个资源。单例对象本身是个小对象还好说,但是如果这个单例对象还是一个“大对象”,这就造成了资源的浪费,还有可能出现频繁gc,这就是大事了!
但是!出现以上问题的原因是什么?根本原因还是没有认真分析到业务场景,书写单例对象本身就是为了让业务进行使用,那这个业务本身就很低频,使用很少,那为什么还要硬去使用单例模式?有必要?应该没有那么必要。
线程安全性
- 线程安全,因为系统启动就加载好了对象,用的时候直接给。
适用场景
- 业务频繁使用。
- 需要满足多线程“安全”,这个安全指的是多线程下这个对象也只有一个。
- 单例对象比较大(因为大,所以才需要程序启动的时候就加载)
代码实现
/**
* 饿汉式
*/
public class HungrySingleton {
// 这个在初始化的时候也可以写在一个静态块里面。这个在启动的时候,就会加载这个静态的对象。
// 不理解为啥程序启动的时候会加载,可以看一下《深入理解java虚拟机》(第三版)
private final static HungrySingleton instance = new HungrySingleton();
// 单例的各种属性,很多属性
private String field1;
private String field2;
private String field3;
private String field4;
private String field5;
private String field6;
private String field7;
private String field8;
private String field9;
private String field10;
// 私有构造器,防止类外进行调用。简单起见就随便赋值了
private HungrySingleton(){
this.field1 = "field1";
this.field2 = "field2";
this.field3 = "field3";
this.field4 = "field4";
this.field5 = "field5";
this.field6 = "field6";
this.field7 = "field7";
this.field8 = "field8";
this.field9 = "field9";
this.field10 = "field10";
}
// 开放方法供外边的类进行单例对象的使用
public static HungrySingleton getInstance(){
return instance;
}
}
懒汉式——写法繁多,分场景看待
- 首先,这里不再写线程不安全的懒汉模式,因为就目前的国内情况来看,各公司基本都是什么高并发,多线程,动不动就千万的QPS(也不知道是不是真的),所以线程不安全的懒汉模式写法,其实用武之地很小很小(除了写着玩)。
- 其次,线程不安全的单例模式,本身就不应该叫做单例模式,因为单例模式是:任何情况下保证只有一个实例。恰巧,多线程也是情况之一。
- 最后,大多数博文(可能是作者想一步步让这个单例变成安全的,一步步分析,给读者更多的解惑),单例模式上来就是线程不安全的懒汉式,很多读者不完整的看一篇博客。结果就是,线程安全的单例没看到,就像每次翻书只看了个开头,就坚持不下去了。翻看的多了,就记住这个不安全的了,面试官让写的时候,就会这个,岂不是很尴尬?所以接下来只介绍线程安全的懒汉单例写法
线程安全的几个懒汉式的写法
枚举型写法——最最应该掌握的一种写法!
枚举型的单例模式是最最推荐的一种写法,因为枚举是从jvm层面直接保证单例的。我们无需考虑任何情况。
枚举类型的单例模式,是最为推荐的一种写法,有以下优点:
- 无需使用任何的锁或者条件判断。多线程方面,jvm就帮我们做了,且是用到的时候才会去加载。
- 保证单例,只会加载一次。
- 书写简单!(这个应该是除了饿汉式的最简单的写法了)
/**
* 最推荐的一种单例模式
*/
public enum EnumSingleton {
// 具体的单例对象
INSTANCE("data1","data2","data3","data4","data5","data6");
// 各种属性。
private Object data1;
private Object data2;
private Object data3;
private Object data4;
private Object data5;
private Object data6;
// 枚举的构造,默认就是private的。外面不可能创建对象。
EnumSingleton(Object data1, Object data2, Object data3, Object data4, Object data5, Object data6) {
this.data1 = data1;
this.data2 = data2;
this.data3 = data3;
this.data4 = data4;
this.data5 = data5;
this.data6 = data6;
}
// 枚举只可以在内部进行实例化,所以在使用的时候,这里通常只开放getter方法
public Object getData1() {
return data1;
}
public Object getData2() {
return data2;
}
public Object getData3() {
return data3;
}
public Object getData4() {
return data4;
}
public Object getData5() {
return data5;
}
public Object getData6() {
return data6;
}
// 返回枚举对象
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
静态内部类写法——最应该掌握的一种写法!
这种写法的原理:依赖类初始化的时候是加锁的来保证线程之间不可见,从而达到线程安全。静态内部类在初始化的时候,依赖jvm的类加载过程中的初始化环节的加锁过程。
/**
* 静态内部类,通过保证类初始化的时候是加锁的来进行线程间不可见,从而达到线程安全
*/
public class LazyStaticInnerClassSingleton {
// 私有构造防止外界直接new对象
private LazyStaticInnerClassSingleton(){}
// 对外开放获取单例对象的方法
public static LazyStaticInnerClassSingleton getInstance(){
// 返回静态内部类中的单例对象
return InnerClassSingleton.instance;
}
// 静态内部类中含有最终的单例对象。
private static class InnerClassSingleton{
private static LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton();
}
}
双重检查机制——应该掌握的一种写法!
这种写法相比上面两个稍微复杂一点点,仅仅是一点点。但是知识点牵涉的不少!
原理:
- synchronized加锁(如果是直接加到方法上面,就不用双重检查了)保证同一时间只有一个线程访问。
- volatile可见性关键字。保证初始化的时候防止指令重排序。
代码:
/**
* 双重检查
*/
public class LazyDoubleCheckSingleton {
// volatile 保证 lazySingleton = new LazyDoubleCheckSingleton() 的操作,不会指令重排
// 具体是因为在对象初始化的时候会有三个指令,分配内存地址,初始化,分配引用地址。这三个指令如果发生指令重排序,那可能就会出问题,具体哪里出问题,可以参考其他博客文章。这里不再赘述。
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if (lazySingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if (lazySingleton == null){
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
源码阅读
jdk
- Runtime.java 饿汉式的单例模式
Spring
- AbstractFactoryBean.java 这个类的getObject方法,会进行单例的判断。
Mybatis
- ErrorContext.java