public class Singleton {
private static volatile Singleton singleton;
public static Singleton getInstance(){
if (singleton==null) {
synchronized (Singleton.class) {
if (singleton==null) {
singleton=new Singleton();
}
}
}
return singleton;
}
}
这里用volatile修饰singleton并不是用了volatile的可见性,而是用了java内存模型的“先行发生”(happens-before)原则的其中一条:
Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”指时间上的先后顺序。
这样一来就能禁止指令重排序,确保singleton对象是在初始化完成后才能被读到。
懒汉模式V3.0可以说是懒汉模式的终极形式,经过2次修改终于线程安全了,然而并没有什么卵用,因为饿汉模式先天就没有线程安全问题,而且也并不像网上说的那样,上来就要创建实例。
饿汉模式解析:
网上一般的说法是,饿汉模式会导致程序启动慢,因为一上来就要创建实例。相信这么说的人一定是不了解java的类加载机制。先上个饿汉模式的代码:
package common;
public class Singleton {
private static final Singleton singleton=new Singleton();
public static Singleton getInstance(){
return singleton;
}
}
可以看到new实例是直接写在了静态变量后面,还有一种写法:
package common;
public class Singleton {
private static final Singleton singleton;
static{
singleton=new Singleton();
}
public static Singleton getInstance(){
return singleton;
}
}
这两种写法在编译后是完全等效的,
类的加载分为5个步骤:加载、验证、准备、解析、初始化
初始化就是执行编译后的<cinit>()方法,而<cinit>()方法就是在编译时将静态变量赋值和静态块合并到一起生成的。
所以说,“饿汉模式”的创建对象是在类加载的初始化阶段进行的,那么类加载的初始化阶段在什么时候进行呢?jvm规范规定有且只有以下7种情况下会进行类加载的初始化阶段:
1.使用new关键字实例化对象的时候
2.设置或读取一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候
3.调用一个类的静态方法的时候
4.使用java.lang.reflect包的方法对类进行反射调用的时候
5.初始化一个类的子类(会首先初始化父类)
6.当虚拟机启动的时候,初始化包含main方法的主类
7.当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
基本来说就是只有当你以某种方式调用了这个类的时候,它才会进行初始化,而不是说jvm启动的时候就初始化,所以说假如你的单例类里只有一个getInstance()方法,那基本上就是当你从其他类调用getInstance()方法的时候才会进行初始化,这事实上和“懒汉模式”是一样的效果。
当然,也有一种可能就是单例类里除了getInstance()方法还有一些其他静态方法,这样当调用其他静态方法的时候,也会初始化实例,但是这个很容易解决,只要加个内部类就行了(这种模式叫holder pattern):
package common;
public class Singleton {
private static class SingletonHolder{
private static Singleton instance=new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
这样只有当调用getInstance()方法的时候,才会初始化内部类SingletonHolder。
总结
经过以上分析,“懒汉模式”实现复杂而且没有任何独占优点,“饿汉模式”完胜。