彻底搞懂单例模式如何安全的实现
Posted 余同学的开发之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了彻底搞懂单例模式如何安全的实现相关的知识,希望对你有一定的参考价值。
单例设计模式,意味着整个系统中只能存在一个实例,比方说像日志对象这种。我们常说的有饿汉式和懒汉式这两种模式来创建单例对象,今天就拓展一下思维,多看几种。
首先我们若是想一个类只有一个对象,那肯定先要私有化构造器,断了在其它的类中使用构造器创建实例的念头。其它的类中不能创建,我们就只能在类中自己创建一个私有实例,另外还要提供一个共有的方法使其它对象获取到实例。所以,第一版出现了。
1 【饿汉式 V1】
在类加载的时候就创建实例
@ThreadSafe
public class SingletonExample2 {
// 私有化构造器
private SingletonExample2(){}
// 提供一个实例
private static SingletonExample2 instance = new SingletonExample2();
// 提供共有的方法返回实例
public static SingletonExample2 getInstance(){
return instance;
}
}
不要忘了在多线程环境中还有关注线程是否安全,我这里都会打上注解,@ThreadSafe 表示线程安全,@NotThreadSafe 表示线程不安全。
上面这种方式就是比较简单的,也是最容易想到的方式,就有一个缺点,若是不使用这个对象,那就有点浪费资源了,这个对象不一定会被使用,但是我们已经创建好了。
2 【饿汉式 V2】
这种方式是借助于 "静态代码块只会被加载一次" 来实现单例的创建,很简单,也很好理解,问题和饿汉式一样,不一定就会使用到这个对象,所以可能会出现浪费资源的情况。
@ThreadSafe
public class SingletonExample6 {
// 私有化构造器
private SingletonExample6(){}
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 提供共有的方法返回实例
public static SingletonExample6 getInstance(){
return instance;
}
}
3 【懒汉式 V1】
在对象使用的时候才创建实例
@NotThreadSafe
public class SingletonExample1 {
// 私有化构造器
private SingletonExample1(){}
// 提供一个实例
private static SingletonExample1 instance = null;
// 提供共有的方法返回实例
public static SingletonExample1 getInstance(){
if(instance == null){
return new SingletonExample1();
}
return instance;
}
}
这种方式在单线程的时候是没有问题的,但是在多线程时就会出现问题,假如线程 A 进入 if 之后暂停执行,此时又来一个线程 B 还是可以进入 if 并返回一个实例,此时 A 再次获得执行时,返回的是另一个实例了。
4 【懒汉式 V2】
在共有方法上添加 synchronized 关键字,同步该方法。可行,但是不推荐使用,因为 synchronized 修饰方法之后,在同一时刻只能有一个线程执行该方法,一旦有线程获得方法,其它线程需要等待,这样会浪费大量时间,系统运行效率降低。
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有化构造器
private SingletonExample3(){}
// 提供一个实例
private static SingletonExample3 instance = null;
// 提供共有的方法返回实例
public static synchronized SingletonExample3 getInstance(){
if(instance == null){
return new SingletonExample3();
}
return instance;
}
}
5 【懒汉式 V3】
这种方式使用双重检测 + 防止指令重排的方式来保证线程安全,首先需要注意的是在 getInstance 方法中,我们需要双层检测并使用同步代码块将创建对象的过程同步起来。
因为在 new SingletonExample4() 的过程中,并不是一个原子操作,是可以进一步拆分为:
1、分配对象内存空间
memory = allocate()
2、初始化对象
initInstance()
3、设置 instance 指向刚分配的内存
instance = memory
在多线程的情况下,上面 3 个指令会存在指令重排序的情况。【JVM 和 CPU 指令优化】重排后的结果可能为:
memory = allocate()
instance = memory
initInstance()
为了防止指令重排带来的问题呢,我们就可以使用 volatile 关键字防止指令重排。这样就是线程安全的了。只需在上一版的基础上使用 volatile 修饰 instance 实例即可。
volatile 的语义就是添加内存屏障和防止指令重排,这在前面已经分析过了。
private static volatile SingletonExample4 instance = null;
6 【使用枚举类实现单例模式】
这是推荐使用的方法,因为它比懒汉式的线程安全更容易保证,比饿汉式的性能高,它只有在调用的时候才实例对象。
@ThreadSafe
@Recommend
public class SingletonSpecial {
private SingletonSpecial(){}
public static SingletonSpecial getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
// public static final Singleton INSTANCE;
private SingletonSpecial singleton;
// JVM 来保证这个构造方法只会调用一次
Singleton(){
singleton = new SingletonSpecial();
}
public SingletonSpecial getInstance(){
return singleton;
}
}
}
7 【使用静态内部类】
这种方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载 SingletonInstance 类,从而完成 Singleton 的实例化。
使用 static final 修饰之后 JVM 就会保证 instance 只会初始化一次且不会改变。
@ThreadSafe
@Recommend
public class SingletonExample7 {
private SingletonExample7(){}
private static class SingletonInstance{
private static final SingletonExample7 instance = new SingletonExample7();
}
public static SingletonExample7 getInstance(){
return SingletonInstance.instance;
}
}
总结一下,今天主要说了单例模式的实现,并且在这中间,复习了一下前面说的线程安全的应用。若是对线程安全的原理以及实现有不懂的可以回头看看这几篇文章。
以上是关于彻底搞懂单例模式如何安全的实现的主要内容,如果未能解决你的问题,请参考以下文章