当Singleton遇到multi-threading

时间:2022-09-23 21:00:10
Singleton可能是所有设计模式中最简单的设计模式啦。它是如此的简单,以至于当我们看着它的UML图的时候会为终于没有了恼人的连线而雀跃,我们 也不用去记忆并体会它有多么强大的功能、多么高深的实现技巧。使用它的理由只有一个――得到一个Class的唯一的Object。它的实现是如此简单,以 至于你会怪我把它的Code列在下面是在浪费服务器空间。但是,天下并没有这么爽的事儿,当Singleton遇到multi-threading,一些 不为人所知的陷阱悄悄的浮上了水面,我们不得不小心应对它们。一下的示例以Java作为编程语言,讨论了multi-threading下实现 Singleton的策略,并顺便告诉你几个关于Java实现Singleton的、你可能不知道的tricky things。

public class Singleton {

    
private static Singleton uniqueInstance;
    
    
private Singleton() {}
    
    
public static Singleton getInstance(){
        
if(uniqueInstance == null){
            uniqueInstance 
= new Singleton();
        }
        
return uniqueInstance;
    }
}

这是典型的Singleton的实现,在single-threading下,它没有任何问题。但是,你能告诉我在multi-threading下,可 能会发生什么事情么?multi-threading是顽皮的小孩,它不保证各个线程执行的顺序,所以,会有一种可能,if (uniqueInstance == null)这个判断会在不同的线程中连续执行。让我们看看最简单的、只有两个线程的情况:
thread 1                             thread 2                             uniqueInstance
if(uniqueInstance == null)                                                null
                                     if(uniqueInstance == null)           null
uniqueInstance = new Singleton();                                         new object
                                     uniqueInstance = new Singleton();    new object

看到了么?产生了两个不同的Object!这不一定永远是坏事,比如这是一个在线购物的控制系统,那么你可能会用一件衣服的钱买到两件衣服,虽然也有可能 是你付了钱,但是没得到衣服,但是概率是50%对50%的,也不是什么大不了的事儿~但是,如果这是一个铁路调度控制系统呢?火车也许会相撞。如果这是一 个核反应堆的控制系统呢?一个城市可能就变成废墟了(当然,也许你希望这发生在日本)。

OK!我们达成了一致,这确实是个大问题。我们怎么解决它?“我们可以用synchronized!”有人兴奋的叫到:“真是个好主意!”好,我们来试试看~
public class Singleton {

    
private static Singleton uniqueInstance;
    
    
private Singleton() {}
    
    
public static synchronized Singleton getInstance(){
        
if(uniqueInstance == null){
            uniqueInstance 
= new Singleton();
        }
        
return uniqueInstance;
    }
}
好样的!现在同一时刻只有一个线程能调用getInstance()方法,不会存在城市被炸平的悲剧了,我们拯救了几百万人(上海的话,一千多万人)!我 代表他们感谢你,但是,过不了多久就有人会站出来抱怨:“为什么系统的速度变慢了?”这些没良心的人们,这么快就忘记了这段code拯救了他们的性命,竟 然抱怨起了这么小的问题!愤愤之余,你开始思考速度变慢的原因。

啊,synchronized,它使得原本并行的访问变成了串行,这是性能的瓶颈,有什么办法可以解决吗?当然,有人站了出来:“我有投机取巧的方法。”
public class Singleton {

    
private static Singleton uniqueInstance = new Singleton();

    
private Singleton() {}

    
public static Singleton getInstance() {
        
return uniqueInstance;
    }
}
问题解决了~JVM会在所有线程之前先实例化一个Object,所有的getInstance()调用都是在引用这个预先产生的Object,不会有任何的问题啦~

但是,完美主义的我们并不满意:“无论是否使用这个Class,都会预先产生一个Object,这很不优雅!”那我们就看看优雅的解决方案。考虑一下 multi-threading下出现错误的原因,并不是因为不同的线程同时调用了getInstance()方法,而是if (uniqueInstance == null)出现了意想不到的排列,也就是说,只有在产生Object的时候,才会有multi-threading的问题,当 uniqueInstance != null时,多少个线程都不会有问题。因此,我们得到了结论:只需要对创建Object的code上锁就可以了。
public class Singleton {

    
private volatile static Singleton uniqueInstance;

    
private Singleton() {}

    
public static Singleton getInstance() {
        
if (uniqueInstance == null) {
            
synchronized (Singleton.class) {
                
if (uniqueInstance == null) {
                    uniqueInstance = 
new Singleton();
                }
            }
        }
        
return uniqueInstance;
    }
}
关键字volatile可以保证uniqueInstance可以被同时运行的若干个线程正确的修改,而getInstance()方法体中的写法则保证 了细力度的线程锁。注意,uniqueInstance == null被判断了两次,这是必要的,这种方法叫做Double-checked locking。

噹噹噹噹~任务完成,Singleton不在惧怕multi-threading啦!让我们来看看关于Java实现Singleton的tricky things。

  • Double-checked locking最好只在Java 5中使用。在Java 1.4以及之前版本的诸多实现中,volatile关键字都被错误的实现,这会导致Double-checked locking的不正确执行。
  • 在Java 1.2以及之前的版本中,JVM的垃圾回收器存在一个bug:当一个Singleton的Object不存在全局引用(即只有自身的instance变量 在引用这Object)的时候,垃圾回收器会回收这个Object。也就是说,在这种情况下,getIntance()会得到一个新的Object,这不会导致城市被炸平,但是会导致做了10年的核反应成果突然消失,当然不是我们想要的。
  • Java允许不同的Class Loader创建自己的Object,它们用namespace来区别这些Object。因此,当你的Singleton设计Class Loader时,最好在获取Object的时候同时指定它的Class Loader。