起源
单例的讲解网上太多了,然而有些点很容易忘,今天又忘了双检索单例的两次条件判断的原因,一到网上查要完全弄清需要花费很长时间,这里记录一下。
双检锁
public class Singleton {
private Singleton() {
}
private static volatile Singleton singleton;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
-
点1 : 私有构造器 为了防止该类实例化,该类只是单例的载体,没有实例化的必要。
-
点2 : 字段singleton为volatile。防止多线程时,指令重排序造成拿到未实例化完成的对象。
我们对singleton = new Singleton()这句话进行分析。共分为3步,
1 在堆中分配一个空间
2 在该空间中实例化对象
3 让singleton字段指向该空间
然而如果没有volatile修饰符的话,重排序有可能会先进行第三步,最后进行第二步。这样就会有一个不完全对象的引用。例如,两个线程A和B,A先进行1的操作,然后进行3的操作,此时A线程让出CPU,B线程开始调用getInstance方法,B线程会发现singleton字段已经不是null了,于是就拿到该单例的引用,但是A线程对单例的实例化还未完全结束,B线程就有可能报错。 -
点3: 第一个判断null是为了提高性能,避免每次调用都加锁,因为对象实例化之后的调用都会发现对象不为null,就直接返回singleton了。
-
点4: 加锁是为了避免同时有多个线程进行实例化。
-
点5: 第二个判断null也是防止多个线程进行实例化。例如A和B两个线程都在第一个判断是否为null的地方判定为null,A线程持有锁,B线程等待A释放锁,这样当A释放锁之后,B线程获得锁,但这时候它会发现singleton已经不为null了,于是就不会再次实例化了。
对于这种方法,没有看过解释的话肯定会觉得莫名其妙,其演进的痕迹就好像我们日常开发一样,发现有漏洞于是想一个弥补措施一样,慢慢就变成了这一奇怪的结构,应该是开发人员想出来的,并不推荐使用这一方法。
静态内部类
public class Singleton {
private Singleton(){}
public Object getInstance(){
return DefalutObject.singleton;
}
private static class DefalutObject{
static final Object singleton = getInstance();
private static Object getInstance(){
return new Object();
}
}
}
-
点1 : 私有构造器 ,为了防止该类实例化。
-
点2 : 静态内部类在被调用的时候才会被加载,实现了懒加载。
-
点3: 类在实例化的时候虚拟机会保证线程安全性,不用我们操心。
这一方法我没记错的话JCIP中也推荐使用,而且java源码中也有例子,例如FileSystems.getDefault()方法就是用的静态内部类。推荐大家使用这种方法。
直接实例化
private static final Object singleton= new Object();
public Object getInstance(){return singleton;}
- 点1 : 直接加载,线程安全性同样由虚拟机保证了。
这一方法才是用的最多的方法,简单易懂。上面两种方法写的那么复杂,就是为了引入懒加载。然而大部分情况下,我们的程序是不需要这么点性能的。
枚举
public enum SingletonEnum {
Singleton;
public Object getInstance(){
return new Object();
}
public static void main(String[] args) {
SingletonEnum.Singleton.getInstance();
}
}
其实,枚举的成员就是枚举对象,只不过他们是静态常量而已。上面的枚举与下面的写法等价。另外,枚举类都继承自Enum这一类,并且是final的。
final class SingletonEnum extends Enum{
public static final SingletonEnum Singleton = new SingletonEnum();
}
枚举的效果与直接实例化效果一样,但要新建一个类,性能不如直接实例化,也不如直接实例化简洁。
总结
如果你的单例真的有延迟初始化的必要的话,例如JAVA的FileSystems类要根据操作系统加载整个Linux或Windows文件系统,大对象延迟初始化是有必要的,这时我建议你使用静态内部类而不是双检索,这也是java语言推荐的写法。如果没有延迟初始化的必要的话,直接实例化简洁明了。
附录
静态内部类延迟初始化的验证,有兴趣的可以看看:
public class Singleton {
public static void main(String[] args) {
Singleton.method();
}
private Singleton(){}
public Object getInstance(){
return DefalutObject.singleton;
}
public static void method(){
System.out.println("这里是外部类的方法");
}
private static class DefalutObject{
public DefalutObject(){
System.out.println("这里是内部类的构造器");
}
static final Object singleton = getInstance();
//加载单例对象
private static Object getInstance(){
System.out.println("这里内部类的单例对象加载方法");
return new Object();
}
}
}
上述main方法的输出结果为下记语句,可以看到没有调用内部类的构造器,也没由调用加载单例对象的方法。