文章目录
前言
昨天回顾了模板方法设计模式,今天回顾一下单例设计模式。单例模式是一种创建类型的常用设计模式,本文介绍五种单例模式实现,同时简要分析为什么有些实现是线程不安全的。
1.饿汉式——线程安全
“饿汉式”,你可以把它可以想象一个“饿汉”,一个很饿的大汉一上来肯定要吃东西,所以饿汉式一开始直接创建类的实例。代码如下:
//饿汉式
public class SingleTon {
//直接创建类的实例,只初始化一次
private static SingleTon instance = new SingleTon();
//私有化构造器
private SingleTon(){
}
//返回创建好的实例
public static SingleTon getInstance(){
return instance;
}
}
优点
- 饿汉式是线程安全的,使用时没有延迟
缺点
- 启动时即实例化,可能会造成资源浪费
2.懒汉式——线程不安全
懒汉式,顾名思义,它很懒,所以一开始并不进行实例化,而是需要的时候才会进行实例化,因为是单例的,所以创建之前进行判断是否为空。代码如下:
//懒汉式——线程不安全
public class SingleTon {
//先不实例化,需要的时候再进行实例化
private static SingleTon instance = null;
//私有化构造器
private SingleTon() {
}
//创建并返回实例对象
public static SingleTon getInstance() {
//判断是否为空
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
}
这种写法在多线程情况下是线程不安全的,会导致多次实例化,违背了单例模式的初衷
TIME | ThreadA | ThreadB |
---|---|---|
T1 | 检测到instance为空, | |
T2 | 检测到instance为空 | |
T3 | 初始化对象B | |
T4 | 返回对象B | |
T5 | 初始化对象A | |
T6 | 返回对象A |
可以看到instance 被实例化了两次,并被不同对象持有,这完全违背了单例设计模式。
优点
- 懒加载启动快,使用时才实例化,不会造成资源浪费
缺点
- 非线程安全
3.懒汉式——线程安全(单锁)
既然涉及到了线程安全,第一反应就是加锁,代码如下:
//懒汉式——线程安全(单锁)
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
//加锁
public static synchronized SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
}
加锁既可以实现延迟加载,又可以实现线程安全,但是因为synchronized为并发排他锁,并发性能差,实际上线程不安全只可能发生在第一次初始化时发生,之后再进行调用就没必要进行加锁。
优点
- 懒加载启动快,使用时才实例化,不会造成资源浪费
- 线程安全
缺点
- synchronized锁会降低性能
4.懒汉式——线程安全(双检锁)
通过分析我们知道仅仅初始化时候需要进行加锁,但是synchronized锁每次都加锁,会使性能减低,为了解决这一问题,下面使用双检锁来保证线程安全,先判断是否已经初始化,在决定要不要加锁。代码如下:
//懒汉式——线程安全(双检锁)
public class SingleTon {
private volatile static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() {
//检测实例是否存在,如果不存在才会进行加锁
if (instance == null) {
//加锁
synchronized (SingleTon.class) {
//再次检测实例是否存在,不存在则进行初始化
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
优点
- 懒加载,线程安全的同时保证性能
注:实例必须有volatile修饰,以保证初始化完全
下面来分析一下为什么必须用volatile修饰,如果不用volatile修饰会发生什么。
instance = new SingleTon();
这句代码分为三个步骤,分别是:
1.分配内存空间
2. 初始化对象
3. 将对象指向刚分配的内存空间
但是有些编译器会将第二步和第三步进行重排序,三个步骤变成了:
1.分配内存空间
2. 将对象指向刚分配的内存空间
3. 初始化对象
然后分析一下在多线程情况下,考虑到重排序,分析如下
TIME | ThreadA | Thread |
---|---|---|
Time1 | 检查到instance为空 | |
Time2 | 获取锁 | |
Time3 | 再次检查到instance为空 | |
Time4 | 为instance分配内存空间 | |
Time5 | 将instance指向分配的内存空间 | |
Time6 | 检查到instance不为空 | |
Time7 | 访问instance(此时instance还未初始化完成) | |
Time8 | 初始化instance |
由上表可知,如果实例对象没被volatile修饰,如果编译器重排序,就会造成在Time7时一样的情况,访问到的是一个为初始化完成的对象。所以必须要在实例对象前加volatile修饰,因为使用了volatile关键字之后,重排序被禁止,所有的写操作都发生在读操作之前,就不会出现上述情况了。
5.静态内部类——线程安全
第五种方式,既有懒加载的有点,又在保证线程安全的同时无需加锁,这种方式通过静态内部类来实现的。代码如下:
/**
* 静态内部类——线程安全
* 静态成员内部类的实例与外部类的实例,没有绑定关系
* 只有被调用才会进行装载,从而实现懒加载
*/
public class SingleTon {
private static class SingleTonHolder {
//静态初始化器,通过JVM来保证线程安全的
private static SingleTon instance = new SingleTon();
}
private SingleTon() {
}
public static SingleTon getInstance() {
return SingleTonHolder.instance;
}
}
优点
- 实现了延迟加载,保证线程安全的同时又避免加锁带来的性能影响
6.枚举类
昨天评论区有人提出没有枚举类的实现方式,今天来加上。《effective java》书中说到“单元素的枚举类型已经成为实现Singleton的最佳方法”。书中讲到“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。”,这个时候为了解决这种这个问题就需要用到枚举类。代码演示如下:
public class SingleTon {
//私有化构造器
private SingleTon() {
}
//定义一个枚举类
enum SingleTonEnum {
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private SingleTon instance;
//枚举类默认且强制私有化构造器
SingleTonEnum() {
instance = new SingleTon();
}
public SingleTon getInstance(){
return instance;
}
}
public static SingleTon getInstance(){
return SingleTonEnum.INSTANCE.getInstance();
}
}
//测试
public class Test {
public static void main(String[] args) {
System.out.println(SingleTon.getInstance());
System.out.println(SingleTon.getInstance());
System.out.println(SingleTon.getInstance() == SingleTon.getInstance());
}
}
运行结果如下:
注:枚举类是实现单例模式的最佳方法,推荐使用
总结
Good Good Study,Day Day Up!