单例模式
这个一个看起来最简单,使用起来却最易出错的模式,因为要完完全全理解这个模式,需要对多线程安全、java内存模型有着很深刻的认识才行。
最原始的实现方式:
public class Singleton{
private final static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
这是最简单也最容易想到的一种实现方式,而且它也确实能够正确工作。但是,它是在类初始化的时候就构造了这个单例对象,而不是在用户第一次请求这个对象时。这对于追求完美的人来说是不可容忍的。行吧,那就改成“延迟构造”的方式吧:
public class Singleton{
private static Singleton instance = null;
public static synchornized Singleton getInstance(){
if(instance == null) instance = new Singleton();
return instance;
}
}
这样虽然解决了前者“浪费资源”的问题,但却引入了新的问题:getInstance()明明只有在第一次访问的时候才会构造对象,其他任何时候调用都是直接返回结果,那这里干嘛要对整个方法同步?简直就是“过多进行了同步”。so,接着改贝:
public class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null) {
synchoronized(Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
这段代码看似解决了问题,实际上却是不能正常工作的。然而却和上一段代码没有本质的区别。比如线程A先调用getInstance(),此时instance为null,则进入同步块准备构造一个Singleton实例。此时线程B调用getInstance(),此时instance仍然为null,于是进入if语句等待进入同步块。于是,表面上是单例模式,实际上每一个线程都拥有了各自的对象。再接着往下改:
public class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null) {
synchoronized(Singleton.class){
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在每次构造之前进行一次判断,这样总该可以了吧。然而这段代码还是不能正常工作的。这其中涉及到了java中著名的DCL失效问题。
java中的线程同步保证一个线程B能够看到另一个线程A的处理结果,但是有个前提是A和B必须是在同一个对象上保持同步。
同时,java程序的执行并不一定会按照源代码的顺序执行,可能会经过指令重排等优化。java对编译器和虚拟机的要求是“as-if-serial ”,即只要能够达到和严格顺序执行时的效果一样,指令执行的先后顺序可以随意安排。
因此,对于上面这段代码,就存在着这么一种可能:线程A调用getInstance(),此时instance为null,则进入同步块构造一个Singleton实例。当它分配了内存给instance,但还没有初始化完成时,线程B调用了getInstance(),此时它判断instance是不为NULL,于是直接返回instance,从而会造成不可预知的结果。
那么,到底还有什么更好的方法可以实现单例模式呢?
如果使用的是JAVA5以后版本的话,可以通过将instance变量设置为volatile。java5以后对volatile语义做了更改。java5以前volatile的语义如下所示:
从上可知,在java5以前使用volatile的实现方式仍然是可能存在并发错误的。java5以后volatile变量的读写操作与synchoronized 有了相同的涵义,可以保证线程安全。但是这么做的话效率就与synchoronized相差无几了。
另一种更好的方法如下:
public class Singleton{
static Class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
改方法巧妙地利用了”java中内部类在第一次使用他时才装载“这一特性,既保证了效率,又避免了同步开销。