目录
引言
开发中什么设计模式最常用? Singleton, Factory, ...
这些常用模式中哪个最简单? Singleton, ...
恭喜你"答对"了! Singleton确实是一个比较"简单"的模式
But -- 肯定要有But的, 不然就没必要有下文了
Singleton如此"简单"的模式很多人却都会犯错!
教科书版本
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // step 1
instance = new Singleton(); // step 2
}
return instance;
}
这个版本最简单, 但是问题也是最多的, 那么有哪些问题呢?
- 线程1: Step 1 (Done) -> Step 2 (Doing)
于此同时
- 线程2: Step 1 (Done, 因为instance == null) -> Step 2
这样就创建了多个实例
Synchronized Method版本
public static synchronized Singleton getInstance() {
if (instance == null) { // step 1
instance = new Singleton(); // step 2
}
return instance;
}
这个版本看起来似乎很安全, 但是
- Synchronized Method同时只能被一个线程调用, 该线程调用结束后才能被其他的线程调用 => 多线程调用效率受到影响
Synchronized Block版本
public static Singleton getSingleton() {
if (instance == null) { // step 1
synchronized (Singleton.class) { // step 2
instance = new Singleton(); // step 3
}
}
return instance;
}
上面的Synchronized Method方法Synchronized的范围是整个方法, 而Synchronized Block方法将Synchronized的范围缩小为Block
看起来算是个改进, 但是却引入了问题
- 线程1: Step 1 (Done) -> Step 2 (Done) -> Step 3 (Doing)
于此同时
- 线程2: Step 1 (Done) -> Step 2 (Waiting)
请注意下面的情节, 此时线程1的Step 3完成了, 即
-
线程1: ... -> Step 3 (Done)
-
线程1: ... -> Step 2 (Done, 此时结束waiting) -> Step 3
结果和教科书版本一样, 又创建了多个实例
Double Checked Locking(双重检验锁)版本
此版本是对上述Synchronized Block版本的改进, 即在Synchronized Block内部又添加了instance == null的判断
public static Singleton getSingleton() {
if (instance == null) { // step 1
synchronized (Singleton.class) { // step 2
if (instance == null) { // step 3
instance = new Singleton(); // step 4
}
}
}
return instance;
}
写到这里的时候, 我已经"厌倦了": 不就写个单例么, 加这么多判断, 同步, 保护难道还有问题不成?!
遗憾的是, 还真是有问题! 问题主要出在Step 4
因为instance = new Singleton()并非是一个原子操作
它由以下三个步骤
-
temp = allocate() => 分配内存
-
constructor(temp) => 构造对象
-
instance = temp => 赋值操作
但JVM存在指令重排序(Re-Order)优化, 导致以上步骤2(构造对象)和步骤3(赋值操作)的顺序并不是固定的!
如果步骤3(赋值操作)先于步骤2(构造对象), 那么有可能发生的问题是
- 线程1: Step 1 (Done) -> Step 2 (Done) -> Step 3 (Done) -> Step 4 (分配内存Done, 赋值操作Done, 构造对象Doing)
于此同时
- 线程2: Step 1 (return, 因为此时instance != null)
但是线程2得到的instance是还没有完全构造的对象, 后果可想而知
Double Checked Locking(双重检验锁)+volatile版本
此版本是对上述Double Checked Locking Pattern版本的改进, 即在instance成员前加上volatile修饰符, 以禁止JVM指令重排序(Re-Order)优化
完整的代码是这样的
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getSingleton() {
if (instance == null) { // step 1
synchronized (Singleton.class) { // step 2
if (instance == null) { // step 3
instance = new Singleton(); // step 4
}
}
}
return instance;
}
好吧, 饶了一大圈之后, 总算又搞定了一个和Synchronized Method版本一样可靠的版本
但是, 可靠不代表效率高, 而且为了创建一个单例, 写上面一大坨代码
又是synchronized, 又是双重判断, 最后连volatile都搬出来了, 你喜欢么?
关于使用volatile修饰符效率的讨论和优化, 详细可以参考Java 单例真的写对了么?
Static Factory版本
上述所有版本要么是有缺陷, 要么是效率低, 但是他们都有个共同的特点: Lazy Loaded(懒加载)
如果不考虑Lazy Loaded带来的这些微小的内存消耗和优化的话, 下面的版本是我最喜欢的
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
现在知道我为什么最喜欢了吧, 因为它真的很简单!
这里的instance成员声明成static final, 这意味着
在该类被加载至内存时就创建了实例, 该过程自然是Thread Safe(线程安全)的
Enum版本
这个版本很"高端", 不过缺点也很明显: 太"高端", 以至于之前我完全没有接触和使用过, 不过为了文章的完整性, 还是在此简单讨论下吧
public enum Singleton{
INSTANCE;
}
什么? 这就完了? 果然太"高端"! 这么神奇, 原理是怎样呢? 黑魔法就是
默认枚举实例的创建是线程安全的
我们可以通过Singleton来访问实例, 使用也是如此简单
附录
更多文章, 请支持我的个人博客
网友评论