原文:http://stackoverflow.com/documentation/java/130/singletons#t=201701151121320953557
Introduction
一个singleton
是一个只有一个实例的类,典型的存储在一个static final 域中, ==
和 equals(Object)
方法对于单例的比较两者没有什么不同(即使你向上或着向下转型),但是对于性能表现==
还是更被倾向。
Examples
一、Enum Singleton
public enum Singleton {
INSTANCE;
public void execute (String arg) {
// Perform operation here
}
}
枚举类型有私有的构造器, 且属性是public 、static、final的,并且提供了合适的序列化机制,非常简介明了且用一种线程安全的方式懒惰加载。
JVM提供了一种保证:枚举值不能被实例化两次,这对枚举型单例模式对于反射攻击提供了强大的防卫。
枚举类型不防范其他的开发者物理的添加其它的元素向代码中。因此,如果你选择这种类型去实现单例,你应该清楚的知道不能在这些枚举中添加新的元素是很有必要的。
这也是被Joshua Bloch in Effective java 所推荐的一种实现单例的方式。
Singleton without use of Enum (eager initialization)
(不用枚举的“饿汉”式加载)
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
这个样例被论证能够有效的进行懒加载 章节12.4.1 of the Java Language Specification 声明:
一个类或着接口类型T 在以下任何一个条件发生之前将会立刻被初始化。
- T是一个类,并且这个类被创建
- T是一个类, 在T中声明的静态方法被调用
- T中声明的A 静态域被赋值
- T中声明的A静态域被使用并且这个域是非常量
T是一个顶级类, 并且T中嵌套的断言声明语句被执行
因此,在类中只要没有其它的静态域或着静态方法,单例将会被初始化直到方法
getInstance()
第一次被调用
Thread-safe lazy initialization using holder class | Bill Pugh Singleton implementation
(使用内部类线程安全的实现单例模式)
public class Singleton {
private static class InstanceHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
private Singleton() {}
}
Singleton.getInstance()
在第一次调用时初始化 INSTANCE
变量,利用了java语言的对静态初始化的线程安全的保证,过程中不要求额外的synchronization。
Thread safe Singleton with double checked locking
(线程安全的双检查锁)
这种类型的单例是安全的,防止了单例被初始化之后 非必要的上锁消耗
public class MySingleton {
// instance of class
private static volatile MySingleton instance = null;
// Private constructor
private MySingleton() {
// Some code for constructing object
}
public static MySingleton getInstance() {
MySingleton result = instance;
//If the instance already exists, no locking is necessary
if(result == null) {
//The singleton instance doesn't exist, lock and check again
synchronized(MySingleton.class) {
result = instance;
if(result == null) {
instance = result = new MySingleton();
}
}
}
return result;
}
}
应该被强调 – 在java SE 5版本之前,这种实现是不正确的,并且应该被避免,在java5版本之前实现双重检查锁定是不可能的 (文末会解释为什么)
译者补充:
####**双重检查锁定的由来**
java程序中,有时候可能需要推迟一些高开销的对象初始化工作,并且只有在使用它的时候才进行初始化,此时程序员会采用延迟初始化。下面是非线程安全的延迟初始化对象的代码:
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1:A线程执行
instance = new Instance(); // 2:B线程执行
return instance;
}
}
在UnsafeLazyInitialization类中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。
对于UnsafeLazyInitialization类,我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化。示例代码如下:
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
由于对getInstance()
方法做了同步处理,synchronized
将导致性能开销,如果getInstance()
方
法被多个线程频繁的调用,将会导致程序执行性能的下降。
在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定(Double-Checked Locking)。人们想通过双重检查锁定来降低同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码:
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始
化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全
其美。
多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
·在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读
取到instance不为null时,instance引用的对象有可能还没有完成初始化
问题的根源
前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
如果2、3步骤进行了重排序:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
这个重排序在没有改变单线程程序执行结果的前提下,可以
提高程序的执行性能,所以被允许了。
那么A、B两个线程下时序如下:
Java内存模型的intra-thread semantics将确保A2一定会排在
A4前面执行。因此,线程A的intra-thread semantics没有改变,但A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。
在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化。
1)不允许2和3重排序。
2)允许2和3重排序,但不允许其他线程“看到”这个重排序。
第一种解决方案是利用volatile的顺序一致性模型的增强语义来禁止2、3步骤重排序。instance 由volatile来修饰:
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance(); // instance为volatile,现在没问题了
}
}r
eturn instance;
}
}
这个解决方案需要JDK 5或更高版本(因为从JDK 5开始使用新的JSR-133内存模
型规范,这个规范增强了volatile的语义)
第二种解决方案就是上面翻译的那个静态内部类实现单例,利用了JVM的特性。