2019独角兽企业重金招聘Python工程师标准>>>
Java单例的常见形式
本文目的:总结Java中的单例模式
本文定位:学习笔记
学习过程记录,加深理解,便于回顾。也希望能给学习的同学一些灵感
一、非延迟加载单例类
public class Singleton {private Singleton() {}private static final Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}
}
二、同步延迟加载
public class Singleton {private static Singleton instance = null;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
三、双重检测同步延迟加载
为了减少同步的开销,于是有了双重检查模式
为处理原版非延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为同步块外面的if (instance == null)可能看到已存在,但不完整的实例。
public class Singleton {private volatile static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized(Singleton.class) { // 1 if (instance == null) { // 2 instance = new Singleton(); // 3 }}}return instance;}
}
双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。
无序写入: 为解释该问题,需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。 什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:
- 线程 1 进入 getInstance() 方法。
- 由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。
- 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。
- 线程 1 被线程 2 预占。
- 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。
- 线程 2 被线程 1 预占。
- 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
mem = allocate(); //为单例对象分配内存空间.
instance = mem; //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance); //为单例对象通过instance调用构造函数
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
在 Java 中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的。对于除了 long 和 double 的基本类型,双重检查模式是适用 的。比如下面这段代码就是正确的:
private int count;
public int getCount(){ if (count == 0){ synchronized(this){ if (count == 0){ count = computeCount(); //一个耗时的计算 } } } return count;
}
上面就是关于java中双重检查模式(double-check idiom)的一般结论。
double-check无效原因可参见
但是事情还没有结束,因为java的内存模式也在改进中。Doug Lea 在他的文章中写道:“根据最新的 JSR133 的 Java 内存模型,如果将引用类型声明为 volatile,双重检查模式就可以工作了”, 参见 。
Section 2.2.7.x on the Memory Model doesn't provide current details of the JSR133 spec revision. (The basic ideas still apply though.) One change that commonly arises in practice is that Section 2.2.7.4 and 2.4.1.2 should say that reading a volatile reference makes visible other changes to the referred object by the thread writing the reference. In particular, double-check idioms work in the expected way when references are declared volatile.
所以以后要在 Java 中使用双重检查模式,可以使用下面的代码:
private volatile Resource resource;
public Resource getResource(){ if (resource == null){ synchronized(this){ if (resource==null){ resource = new Resource(); } } } return resource;
}
四、使用内部类实现延迟加载(推荐)
推荐方法Initialization-on-demand holder idiom
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; }
}
其他
以上为常见单例形式,另有 ThreadLocal、枚举 等实现形式
此处不作介绍
参考
- http://www.cnblogs.com/dolphin0520/p/3920373.html#!comments
- http://www.iteye.com/topic/652440
- http://www.iteye.com/topic/260515
- https://blog.csdn.net/dl88250/article/details/5439024
- https://blog.csdn.net/chenchaofuck1/article/details/51702129
对本文有什么建议(内容、写作风格等),欢迎留言提出,感谢!