作者 | 张新强
链接 | www.barryzhang.com/archives/521
1. 前言
单例(Singleton)应该是开发者们最熟悉的设计模式了,并且好像也是最容易实现的——基本上每个开发者都能够随手写出——但是,真的是这样吗?
作为一个Java开发者,也许你觉得自己对单例模式的了解已经足够多了。我并不想危言耸听说一定还有你不知道的——毕竟我自己的了解也的确有限,但究竟你自己了解的程度到底怎样呢?往下看,我们一起来聊聊看~
2. 什么是单例?
单例对象的类必须保证只有一个实例存在——这是维基百科上对单例的定义,这也可以作为对意图实现单例模式的代码进行检验的标准。
对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:
-
懒汉式: 指全局的单例实例在第一次被使用时构建。 -
饿汉式: 指全局的单例实例在类装载时构建。
懒汉式
的单例,毕竟按需加载才能做到资源的最大化利用嘛。
3. 懒汉式单例
3.1 简单版本
// Version 1
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
// Version 1.1
public class Single1 {
private static Single1 instance;
private Single1() {}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。
3.2 synchronized版本
synchronized
:
// Version 2
public class Single2 {
private static Single2 instance;
private Single2() {}
public static synchronized Single2 getInstance() {
if (instance == null) {
instance = new Single2();
}
return instance;
}
}
synchronized
关键字之后,getInstance方法就会锁上了。
如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。
——所以这端代码也就避免了Version1中,可能出现因为多线程导致多个实例的情况。
3.3 双重检查(Double-Check)版本
// Version 3
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
if (instance == null)
的判断,这个叫做『双重检查 Double-Check』。
-
第一个 if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。 -
第二个 if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况。
原子操作
、
指令重排
。
知识点:什么是原子操作?
原子操作(atomic)
就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
m = 6; // 这是个原子操作
int n = 6; // 这不是一个原子操作
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
知识点:什么是指令重排?
int a ; // 语句1
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
指令重排
的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
原子操作
和
指令重排
的概念之后,我们再继续看Version3代码的问题。
下面这段话直接从陈皓的文章(深入浅出单实例SINGLETON设计模式)中复制而来:
主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。 也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。 如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
if (instance == null)
这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
3.4 终极版本:volatile
volatile
关键字即可,Version4版本:
// Version 4
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}
volatile
关键字的一个作用是禁止
指令重排
,把instance声明为
volatile
之后,对它的写操作就会有一个
内存屏障
(什么是内存屏障?
),这样,在它的赋值完成之前,就不用会调用读操作。
注意: volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作( if (instance == null))。
好了,现在彻底没什么问题了吧?
EventBus.getDefault()
就是用这种方法来实现的。
4. 饿汉式单例
饿汉式
单例是指:
指全局的单例实例在类装载时构建的实现方式。
4.1 饿汉式单例的实现方式
饿汉式
单例的实现如下:
//饿汉式实现
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}
-
可能由于初始化的太早,造成资源的浪费 -
如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。
知识点:什么时候是类装载时?
2. 使用反射创建它的实例时
3. 子类被加载时,如果父类还没被加载,就先加载父类
4. jvm启动时执行的主类会首先被加载
5. 一些其他的实现方式
5.1 Effective Java 1 —— 静态内部类
// Effective Java 第一版推荐写法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
-
对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。 -
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
5.2 Effective Java 2 —— 枚举
// Effective Java 第二版推荐写法
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();
这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。 虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
6. 总结
【END】
推
荐
阅
读
1. 我采访了一位 Pornhub 工程师,聊了这些纯纯的话题
2.
常见排序算法总结 - Java 实现
3. Java:如何更优雅的处理空值?
4. MySQL:数据库优化,可以看看这篇文章

喜欢文章,点个
在看
本文深入探讨了单例模式的多种实现方式,包括懒汉式、饿汉式、静态内部类及枚举方法,分析了每种方法的优缺点,特别关注了线程安全性和效率问题。
337

被折叠的 条评论
为什么被折叠?



