场景描述:假设我们需要一个全局唯一的实例,比如线程池,缓存等。
初步解决:比如所有开发人员通过约定,或者全局变量来实现。
问题:将对象赋值给一个全局变量,但并不一定会立即使用它,从而占用内存资源,造成资源的浪费。
更好的方案:使用单例模式。
经典的单例模式代码如下:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
单件模式确保一个类只有一个实例,并提供一个全局访问点。
这种做法对资源敏感的对象特别重要。
相应的类图:
新的问题:假设我们的程序是一个多线程的程序,如果使用上述经典代码就会出现多个单例对象的奇特情况。因为我们缺少了同步。
第一种解决方案:将方法变为同步方法。
public class Singleton_One {
private static Singleton_One uniqueInstance;
private Singleton_One(){}
private static synchronized Singleton_One getInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton_One();
}
return uniqueInstance;
}
}
这种方法带来的缺陷是每次访问方法都会进行同步,造成性能的下降。而我们实际上只需要在第一次进行同步即可。
第二种方法,不使用延迟实例化,而是急切的创建实例。
public class Singleton_Two {
private static Singleton_Two uniqueInstance = new Singleton_Two();
private Singleton_Two(){}
public static Singleton_Two getInstance(){
return uniqueInstance;
}
}
这种方法的缺陷类似于使用全局变量的问题,虽然提前创建了实例,但我们并不能保证创建了后就立即使用,也会造成一定程度的浪费。
第三种方法,双重检查加锁,曾经的一种经典做法。
public class Singleton_Three {
private volatile static Singleton_Three uniqueInstance;
private Singleton_Three(){}
public static Singleton_Three getInstance(){
if (uniqueInstance == null){
synchronized (Singleton_Three.class){
if (uniqueInstance == null){
uniqueInstance = new Singleton_Three();
}
}
}
return uniqueInstance;
}
}
只有在第一次进行同步,并且使用了volatile变量避免了重排序造成的问题,但一方面由于其自身的缺陷,另一方面同步性能问题的提升,导致这种解决方案已经变为历史,不推荐使用,复杂且难以理解。
第四种方法,延迟初始化占位类。
public class Singleton_Four {
private static class ResourceHolder{
public static Resource resource = new Resource();
}
public static Resource getResource(){
return ResourceHolder.resource;
}
}
public class Resource {
// 一个资源类,需要被单例加载
}
当任何线程第一次调用getResource时,都会使ResourceHolder被加载和被初始化,此时静态初始化器将执行Resource的初始化操作。这种方法在第二种提前实例化的方法上改进而来,并且不需要同步。因此最推荐在多线程情况下使用该方法。
注意:每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。如果这样的事情发生在单例上,就会产生多个单例并存的怪异现象。所以如果你的程序有多个类加载器又同时使用了单例模式,请小心。
总结:当你的程序中迫切需要在全局只需一个实例对象的情况时,考虑使用单例模式,并推荐使用延迟初始化占位类方式避免多线程问题。但过度使用单例模式有可能适得其反,所以在设计时需要慎重考虑。