Singleton指仅仅被实例化一次的类
第一种:最简单的实现
先把类的构造函数私有化(private),从而保证别的类不能实例化此类(因为没有指向实例化对象的引用),接着用public static final修饰成员变量instance,以供外部使用者调用,获取SimpleSingletonClass的实例。代码如下:
public class SimpleSingletonClass {
public static final SimpleSingletonClass instance = new SimpleSingletonClass();
private SimpleSingletonClass() {
}
}
或者私有化instance,代码如下:
public class SimpleSingletonClass {
private static final SimpleSingletonClass instance = new SimpleSingletonClass();
public static SimpleSingletonClass getInstance() {
return instance;
}
private SimpleSingletonClass() {
}
}
又或者使用静态代码块的方式,代码如下:
public class SimpleSingletonClass {
private SimpleSingletonClass instance = null;
static{
instance = new SimpleSingletonClass();
}
public static SimpleSingletonClass getInstance() {
return instance;
}
private SimpleSingletonClass() {
}
}
也还有其它的一些写法。
PS:在类被加载时便实例化instance,从而避免了多线程的同步问题,但应用中存在多个类加载器时会导致单例失败。
2、性能优化--延迟加载(lazy loaded)
上面的单例方式有一个问题:此类被加载后,无论此类是否被引用,都会创建一个instance对象。如果这个对象创建很耗时,又或者有很多类似这样的类呢?并且这些类不一定会被使用,那么就会造成资源浪费,项目大的情况下问题就更严重了。此时解决的办法是优化代码--延迟加载(lazy loaded),代码如下:
public class OptimizeSingletonClass {
private static OptimizeSingletonClass instance = null;
public static OptimizeSingletonClass getInstance() {
if(instance == null) {
instance = new OptimizeSingletonClass();
}
return instance;
}
private OptimizeSingletonClass() {
}
}
类被加载后,instance的值为null,并没有创建OptimizeSingletonClass对象,OptimizeSingletonClass对象只有在第一次调用getInstance()方法时才会被创建,这便是延迟加载。
同步
“80%的错误都是由20%代码优化引起的”。单线程下,上面这种延迟加载的方式是没问题的,但在多线程下,问题就来了--有线程A和线程B想使用OptimizeSingletonClass,由于getInstance()方法是第一次被调用,线程A检测发现instance的值为null,此时CPU发生时间片切换,线程B开始执行,也检测发现instance的值为null,因此线程B开始创建,创建完对象后,切换到线程A,因为线程A已经检测完了,不再重新检测就创建对象。如此线程A和线程B就各自拥有一个OptimizeSingletonClass对象--单例失败!
解决的办法是给getInstance()方法加锁-synchronized--效率较低:一个线程必须等另一个线程释放方法的锁后才有机会使用,就像进浴室关门上锁洗澡一样,只有你出来了,后面的人才有机会使用。代码如下:
public class OptimizeSingletonClass {
private static OptimizeSingletonClass instance = null;
public synchronized static OptimizeSingletonClass getInstance() {
if(instance == null) {
instance = new OptimizeSingletonClass();
}
return instance;
}
private OptimizeSingletonClass() {
}
}
3、又是性能优化
上一个例子中的getInstance()方法加锁后,性能会下降,为了提高性能,能不能只为某一段代码加锁呢?研究代码分析知原因是检测instance的值是否为null的操作和创建对象的操作分离了。假如把这两个操作锁起来,那就能保证单例的实现了。那么,如下的代码实现方式行?
public class OptimizeSingletonClass {
private static OptimizeSingletonClass instance = null;
public static OptimizeSingletonClass getInstance() {
synchronized (OptimizeSingletonClass.class) {
if(instance == null) {
instance = new OptimizeSingletonClass();
}
}
return instance;
}
private OptimizeSingletonClass() {
}
}
每次调用getInstance()方法时,依然需要进行同步,所以这种方式是没作用的,我们能不能根据instance的值判断是否需要加锁?代码如下:
public class OptimizeSingletonClass {
private static OptimizeSingletonClass instance = null;
public static OptimizeSingletonClass getInstance() {
if (instance == null) {
synchronized (OptimizeSingletonClass.class) {
if (instance == null) {
instance = new OptimizeSingletonClass();
}
}
}
return instance;
}
private OptimizeSingletonClass() {
}
}
第二次检查instance的值是否为null的原因:假如A、B两个线程都已经检测到instance的值为null了,若是线程A和线程B先后进入同步块后都不再检测,获取的OptimizeSingletonClass对象又都是不相同的了。
但这种Double-check locking方式是否真的完美了呢?答案是--NO。
追根溯源--编译原理
编译,就是把源代码“翻译”成目标代码(多指机器代码)的过程。但对Java而言,它的目标代码是虚拟机代码--字节码。编译原理里面有一个很重要的内容是编译器优化。编译器优化:在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。然而JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
创建一个变量需要哪些操作呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。然而,JVM规范并没有规定这两个操作的先后顺序。这时就有这么一种情况:线程A开始创建OptimizeSingletonClass的实例,此时线程B调用了getInstance()方法,首先检测instance是否为null。按照内存模型,线程A已经把instance指向了那块内存,只是还没有调用构造方法,因此线程B检测到instance不为null,于是直接返回instance--问题出现了,尽管instance不为null,但它并没有构造完成,如果此时线程B在A将instance构造完成之前就是用了instance实例,程序就会出现错误!
那如下的实现方式是否可行呢?
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
SingletonClass sc;
synchronized (SingletonClass.class) {
sc = instance;
if (sc == null) {
synchronized (SingletonClass.class) {
if(sc == null) {
sc = new SingletonClass();
}
}
instance = sc;
}
}
}
return instance;
}
private SingletonClass() {
}
}
写此代码的原因:synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。
不过,这种想法依然是错误的!同步块的释放保证在同步块里面的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,就相当于在前面程序加入了一个临时变量而已,但是这临时变量又陷入相同的状况里面了,所以程序又是错误的了!难道没有解决方案了?
JDK1.5之后,关键字volatile解决另了这个问题,代码如下:
public class OptimizeSingletonClass {
private volatile static OptimizeSingletonClass instance = null;
public static OptimizeSingletonClass getInstance() {
if (instance == null) {
synchronized (OptimizeSingletonClass.class) {
if (instance == null) {
instance = new OptimizeSingletonClass();
}
}
}
return instance;
}
private OptimizeSingletonClass() {
}
}
4、静态内部类--实现延迟加载的另一种方式
代码如下:
public class SingletonClass {
private static final InnerClass {
private static final instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return InnerClass.instance;
}
private SingletonClass() {
}
}
5、极推荐的方案
上面的例子若是要实现序列化,则需要实现Serializable接口,并把所有的实例域声明为瞬时(transient)的,还要提供一个readResolve方法才能实现Singleton类的可序列化。另外,还要处理反射攻击问题。
下面是jdk1.5之后的方案:只需要编写一个包含单个元素的枚举类型。代码如下:
public enum SingletonClass {
INSTANCE; //代表一个SingletonClass实例
}
这种方案“更加简洁,并且无偿的提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。”--引自Effective Java 中文版第二版。
参考资料:
Effective Java 中文版第二版
深入理解单例模式(上)http://www.importnew.com/29338.html
深入理解单例模式(下)http://www.importnew.com/29343.html
单例模式的7种写法 http://cantellow.iteye.com/blog/838473
可以不要再使用Double-Checked Locking了 http://www.importnew.com/23980.html