设计模式(Design pattern)一共有23种,而单例模式(Singleton Pattern)是 其中最简单的设计模式之一,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。单例模式有很多种写法,下面我们介绍一下常见的7种写法。
说明
单例模式有几个注意点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这个实例
常见的单例模式主要分为懒汉式、饿汉式,这两种又衍生出了很多种写法。还有一种写法是单例的枚举模式,本文将逐一介绍。
懒汉模式
懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。它有线程安全和不安全的两种写法,下面我们由浅入深,逐一讨论。
1. 单例实例在第一次使用时进行创建
这是一种比较常规和简单的写法,值得注意的是它的构造方法是私有的,一个线程不安全的写法,多线程环境下,可能运行多次就会new多次,会产生一定的影响。
@Slf4j
@NotThreadSafe
public class SingletonDemo1 {
//私有构造函数,让构造函数为 private,这样该类就不会被实例化
//如果不将它们设为私有,则将创建多个Singleton类实例,保证了单例类只能有一个实例
private SingletonDemo1() {
}
//单例对象
private static SingletonDemo1 instance = null;
//静态工厂方法获取对象
public static SingletonDemo1 getInstance() {
//在单线程模式下没有问题,多线程下,可能运行多次就会new多次,会产生一定的影响
if (instance == null) {
instance = new SingletonDemo1();
}
return instance;
}
}
2.通过加入 synchronized 关键词实现线程安全的懒汉式
通过加入 synchronized 关键词可以实现线程安全的,但是加入synchronized之后失去了并发性,同一时刻只允许一个线程访问。优点:线程安全,缺点:当然是缺乏并发性,给性能带来一定的开销。
@Slf4j
@ThreadSafe
@NotRecommend
public class SingletonDemo3 {
//私有构造函数
private SingletonDemo3() {
}
//单例对象
private static SingletonDemo3 instance = null;
//静态工厂方法获取对象
//加上synchronized关键字
public static synchronized SingletonDemo3 getInstance() {
if (instance == null) {
instance = new SingletonDemo3();
}
return instance;
}
}
3.懒汉模式的双重同步锁代理模式(线程不安全)
懒汉模式的另一种写法是双重同步锁代理模式,又称双重检测机制 (DCL),如下:
@Slf4j
@NotThreadSafe
public class SingletonDemo4 {
//私有构造函数
private SingletonDemo4() {
}
//单例对象
private static SingletonDemo4 instance = null;
//静态工厂方法获取对象
public static SingletonDemo4 getInstance() {
if (instance == null) {//双重检测机制 DCL //B
synchronized (SingletonDemo4.class) { //同步锁
if (instance == null) {
instance = new SingletonDemo4(); //A -3
}
}
}
return instance;
}
}
我们在getInstance()里面又加了一个双重检测校验,这种方式在单线程下面没有问题,而在多线程情况下就会不安全了,一般情况下它的执行顺序为:
- memory = allocate() 分配对象的内存空间
- ctorInstance()初始化对象
- instance = memory 设置instance指向放分配的内存
由于可能会出现指令重排的情况,由于jvm和cpu优化,发生了指令重排,执行顺序就可能为:1、3、2。
例如: 两个线程,线程A、B同时调用这个getInstance方法,A执行到了下面A的位置,而B执行到了下面B的位置,如果按照上面说的指令重排的1,3,2顺序 A执行了3,而B线程来了之后,发现A线程已经有值了(但是它还没执行2对其进行初始化操作),然后直接返回了,就会出问题了。此刻,我们需要在当前这种情况下,防止指令重排,就引出了volatile关键字。
4.懒汉模式的双重同步锁代理模式(线程安全)
限制它产生指令重排,用volatile关键字,如下:
@Slf4j
@ThreadSafe
public class SingletonDemo5 {
//私有构造函数
private SingletonDemo5() {
}
/**
* 1、memory = allocate() 分配对象的内存空间
* 2、ctorInstance()初始化对象
* 3、instance = memory 设置instance指向放分配的内存
*/
//单例对象 volatile + 双重检测机制 -> 禁止指令重排
private static volatile SingletonDemo5 instance = null;
//静态工厂方法获取对象
public static SingletonDemo5 getInstance() {
if (instance == null) {//双重检测机制 DCL //B
synchronized (SingletonDemo5.class) { //同步锁
if (instance == null) {
instance = new SingletonDemo5(); //A -3
}
}
}
return instance;
}
}
恶汉模式
5.常规恶汉模式写法
恶汉模式它在类装载使用时进行创建(能保证线程安全),同时也会引入一个缺点,就是如果类的构造方法中存在着过多的处理,会导致这个类加载的时候特别慢。
@Slf4j
@ThreadSafe
public class SingletonDemo2 {
//饿汉模式缺点--如果类的构造方法中存在着过多的处理,会导致这个类加载的时候特别慢
//私有构造函数
private SingletonDemo2() {
}
//单例对象
private static SingletonDemo2 instance = new SingletonDemo2();
//静态工厂方法获取对象
public static SingletonDemo2 getInstance() {
return instance;
}
}
6.通过静态代码块方式的汉模式
通过静态代码块的方式来实例化类对象,这里有点需要注意,就是在写静态域或者静态代码块的时候,我们要注意顺序,如下面1和2顺序不能颠倒,颠倒了之后他们的执行结果会有所不同。
@Slf4j
@ThreadSafe
public class SingletonDemo6 {
//饿汉模式缺点--如果类的构造方法中存在着过多的处理,会导致这个类加载的时候特别慢
//私有构造函数
private SingletonDemo6() {
}
//单例对象
//当我们在写静态域或者静态代码块的时候,我们要注意顺序,如下面1和2顺序不能颠倒,
//他们的执行结果会有所不同
//1
private static SingletonDemo6 instance = null;
//2
static {
instance = new SingletonDemo6();
}
//静态工厂方法获取对象
public static SingletonDemo6 getInstance() {
return instance;
}
public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
枚举模式
7.枚举方式的单例模式
说实话,这种方式用的比较少,但是还是比较推荐的一种写法,一来它是线程安全的。它相比于我们的懒汉模式在安全性方面得到了保证,它相比于饿汉模式,在实际调用的时候才做初始化
@Slf4j
@ThreadSafe
@Recommend
public class SingletonDemo7 {
//私有构造函数
private SingletonDemo7() {
}
public static SingletonDemo7 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonDemo7 singleton;
//JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonDemo7();
}
public SingletonDemo7 getInstance() {
return singleton;
}
}
}
总结
本文总结了7种单例模式的具体写法,有浅入深。从最简单的懒汉模式入手,引出了线程安全的实现方式。从DCL的懒汉模式的cpu指令重排,又引入了volitate关键字。此外,还讲了恶汉模式的常规写法,以及通过静态代码块的写法。最后,举了比较推荐的枚举方式的单例模式。
参考:https://github.com/nuptkwz/notes/tree/master/technology/designpattern