其他
我们再来聊一聊 Java 的单例吧
作者 | 张新强
链接 | 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】