单例分为懒汉、饿汉模式。前者的实现比较多,但用于应用中不是必须使用的对象。后者实现简单。以下对比较成熟的实现方式做些介绍。
一、懒汉
1.双重校验锁
public class SingletonDoubleSyn {
//防止JVM指令重排序,导致别的线程拿到instance!=null 但是又万未被初始化完成的对象
private static volatile SingletonDoubleSyn instance;
private SingletonDoubleSyn(){}
public static SingletonDoubleSyn getInstance(){
if (instance == null) {//1
synchronized (SingletonDoubleSyn.class) {
if(instance == null) {
instance = new SingletonDoubleSyn();//2
}
}
}
return instance;
}
}
这里需要注意3点:
(1)、instance变量要加volatile关键字,为了防止JVM重排序。
编译器会将这段代码 instance = new SingletonDoubleSyn();//2 编译成3句JVM指令:
memory = allocate(); // 1 给instance对象分配内存
init(memory); // 2 初始化刚刚分配的内存
instance = memory; // 3 将分配的内存地址赋值到instance中
不过这2、3两条语句的执行顺序是不确定的,会在编译时指令重排序,如下所示:
memory = allocate(); // 1 给instance对象分配内存
instance = memory; // 3 将分配的内存地址赋值到instance中
init(memory); // 2 初始化刚刚分配的内存
线程A执行到3的位置,此时恰好切换到线程B,B先判断instance == null,返回false,那此时B线程拿到的是为完成初始化的instance对象。
这一情况在变量前加上volatile可以避免。
(2)、因为绝大多数对instance的访问是读,因此只有在instance==null是才需要线程同步。
(3)、将构造方法私有化,保证单例。
2.静态内部类
public class SingletonByInnerClass {
private SingletonByInnerClass(){}
private static class SingletonHolder{
//这里为什么要final修饰?
private static final SingletonByInnerClass instance = new SingletonByInnerClass();
}
public static SingletonByInnerClass getInstance(){//1
return SingletonHolder.instance;
}
}
这里通过JVM的类加载机制(JVM对加载类时,做了同步机制),巧妙地避免了线程间的竞争。
声明一个变量不会触发JVM加载对应类,而当访问了类静态方法和静态变量时,会触发JVM加载类,类加载完成后会执行由编译器根据类中static变量,static方法,static块生成的init()方法,来初始化类。
不明白静态内部类中的变量为何要用final修饰?
JVM在对.class文件做了合法校验后,会在内存中(方法区)开辟一块区域,用以存放类变量,开辟完成后,会对新开辟的内存区域“清零”。如果一个变量被final修饰,该变量就在此时被赋值;如果没有final修饰,他会在对类的初始化中被赋值。无论哪种都能保证赋值,并不会被提前发布(不安全发布)。为什么要加final?
二、饿汉
public class SingletonHungry {
private static SingletonHungry instance = new SingletonHungry();
private SingletonHungry(){}
public static SingletonHungry getInstance() {
return instance;
}
}