1. 普通实现(线程不安全)
public class SingleDemo1 {
private static SingleDemo1 instance = null;
// 私有构造
private SingleDemo1() {
// do something
}
/**
* fixme: 线程不安全,可能会重复创建,导致实例被覆盖
*/
public static SingleDemo1 getInstance() {
if (instance == null) {
instance = new SingleDemo1();
}
return instance;
}
}
2. 简单加锁实现(锁开销较大)
public class SingleDemo2 {
private static SingleDemo2 instance = null;
// 私有构造
private SingleDemo2() {
// do something
}
/**
* fixme: 每次获取实例都要加锁串行,开销较大
*/
public static SingleDemo2 getInstance() {
synchronized(SingleDemo2.class) {
if (instance == null) {
instance = new SingleDemo2();
}
}
return instance;
}
}
3. double check + synchronized
public class SingleDemo3 {
private static SingleDemo3 instance = null;
// 私有构造
private SingleDemo3() {
// do something
}
public static SingleDemo3 getInstance() {
if (instance == null) {
synchronized(SingleDemo3.class) {
if (instance == null) {
instance = new SingleDemo3();
}
}
}
return instance;
}
}
内外双层检查,外层存在竞态,内层可能会因为指令重排导致返回未初始化的对象。解决了可见性,但是未考虑有序性。
原始是因为,将instance = new SingleDemo3()
这句转化为伪代码指令可表示为:
objRef = allocate(SingleDemo3.class); // 子操作1: 分配对象所需要的存储空间
invokeConstructor(objRef); // 子操作2: 初始化objRef引用的对象
instance = objRef; // 子操作3: 将对象写入共享的变量
由于JIT编译器可能将上述的子操作重排序为:子操作①→子操作③→子操作②,即在初始化对象之前将对象的引用写入实例变量instance。由于锁对有序性的保障是有条件的,而操作①(第1次检查)读取instance变量的时候并没有加锁,因此上述重排序对操作①的执行线程是有影响的:该线程可能看到一个未初始化(或未初始化完毕)的实例,即变量instance的值不为null,但是该变量所引用的对象中的某些实例变量的变量值可能仍然是默认值,而不是构造器中设置的初始值。也就是说,一个线程在执行操作①的时候发现instance不为null,于是该线程就直接返回这个instance变量所引用的实例,而这个实例可能是未初始化完毕的,这就可能导致程序出错!
4. double check + synchronized + volatile
针对3中的实现,只需要给共享变量instance
添加volatile关键字即可。volatile关键字解决了两个问题:
- 保障可见性:任意线程通过写操作修改了instance的指向,其它线程也可以读取到对应的修改。
- 保障有序性:由于volatile关键字能够禁止volatile变量写操作与该操作之前的任何读写进行重排序,因此就不存在将子操作2重排到子操作3之后的情况。
public class SingleDemo4 {
// 添加volatile修饰
private volatile static SingleDemo4 instance = null;
// 私有构造
private SingleDemo4() {
// do something
}
public static SingleDemo4 getInstance() {
if (instance == null) {
synchronized(SingleDemo4.class) {
if (instance == null) {
instance = new SingleDemo4();
}
}
}
return instance;
}
}
5. 静态内部类
除了上面的方式之外,还有其它的单例实现模式,比如静态内部的方式实现。
当外部调用getInstance方法时,Inner内部类会被jvm加载,由于instance是静态变量,所以会被一同初始化,以此达到了lazy初始化的同时也保证了线程安全。
public class SingleDemo5 {
private static class Inner {
final static SingleDemo5 instance = new SingleDemo5();
}
// 私有构造
private SingleDemo5() {
// do something
}
public static SingleDemo5 getInstance() {
return Inner.instance;
}
}
6. 枚举实现
枚举类型SingleDemo6相当于一个单例类,其字段INSTANCE值相当于该类的唯一实例。这个实例是在SingleDemo6.INSTANCE初次被引用的时候才被初始化的。仅访问SingleDemo6本身(比如上述的SingleDemo6.class.getName()调用)并不会导致SingleDemo6的唯一实例被初始化。
public enum SingleDemo6 {
INSTANCE;
// 私有构造
SingleDemo6() {
// init
}
// 外部通过枚举实例访问即可
public void doSomething(){
// do something
}
}
引用自:黄文海. Java多线程编程实战指南(核心篇) (Java多线程编程实战系列) (Chinese Edition) (Kindle Locations 2435-2437). 电子工业出版社. Kindle Edition.