在某些情况下,比如维护数据库连接池,网站计数器等对象时,我们希望我们的对象只有一个。然而我们又不能指望开发人员的单例约定。单例模式是这样一种方法可以使得程序执行过程中,类对象只有一个实例。
一个简单的方法是在类中维护一个类静态对象,而只能通过一个静态方法去获取它。像下面这样:
public class Singleton {
private static Singleton uniqueInstance;
public static Singleton getUniqueInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
用户只能通过getUniqueInstance
方法获取单例对象,第一次获取时将会新创建一个对象。
这样的方式在大多数情况下是可用的,而在多线程的情况下怎会出现问题,如线程A和线程B同时调用了此方法,而此时uniqueInstance对象为null,则两个线程都将创建一个单例对象,从而引发错误。
一个解决办法是,使用synchronized关键字使得getUniqueInstance
方法是线程同步的,即调用此方法时将会等待其他线程执行完毕。代码如下:
public static synchronized Singleton getUniqueInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
上述方法解决了正确性的问题,然而,在多个线程都要调用getUniqueInstance
方法时则会带来严重的性能问题,因为每一次调用都要等待其他线程执行完毕以保证单例正确,这样可能就无法发挥多线程的性能优势。
考虑以上代码,我们需要同步的点只有单例的创建过程,当uniqueInstance
对象已经被创建之后,上述函数是不需要被同步的。所以我们做出如下优化:
public static Singleton getUniqueInstance2(){
if(uniqueInstance == null){
synchronized (Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
上述代码先检查是否已经存在单例对象,如果存在则直接返回。否则则进入同步代码块,进入同步代码块之后再次检查单例对象是否存在,以防在等待同步过程中其他线程已经创建了单例对象。
上述通过同步线程的方式保证单例正确性的方法存在一个严重的问题,在线程执行过程中,CPU可能会为了性能优化而把自己线程中的对象保存cache中,而执行在其他CPU的线程则还是无法获取已经被创建的对象。这就需要使用volatitle关键字修饰 uniqueInstance对象,此关键字保证每一次写(赋值)都会将对象写到计算机主内存中,而每一次的读都从主内存中读。(详细信息查看 java volatitle)
所以我们的uniqueInstance
将这样声明
private static volatile Singleton uniqueInstance;
从前面的代码中可以看出,我们每次都是检查单例是否存在。而如果在程序设计中,我们可以确定一定或具有很大的可能性去创建一个单例对象,那么我们可以使用如下这样简答的方法:
private static Singleton singleton = new Singleton();
public static Singleton getSingleton(){
return singleton;
}
这种情况下在类Singleton加载时便初始化了属性singleton,则不用考虑复杂的线程问题。