- 基本的单例模式,分饿汉模式和懒汉模式,饿汉模式在定义变量时就用实例化对象赋值,
getInstance()
方法中直接返回变量;懒汉模式在定义变量时用null
赋值,getInstance()
方法中需要判空,如果变量值为null
的话需要赋值。可以简单理解为饿汉自己寻找东西吃,懒汉需要别人喂食。
饿汉模式不存在线程安全的问题,因为在加载类时就已经对其变量赋值。懒汉模式存在线程安全的问题。如果两个线程同时通过了if(instance == null)
判断条件,开始执行new
操作,这样一来,显然instance
被构建了两次。
// 饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
//懒汉模式
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
2. 使用synchronized
同步锁和双重检测机制(即判断两次)是一般解决线程安全问题的方法。但是此方法在字节码层面还是存在线程安全的问题(指令重排时可能出现问题)。
指令重排是指一句简单的Java语句,在编译为字节码时可能会经过JVM的优化调整字节码指令顺序。比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:如一条new
指令会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
有可能会被JVM调整为如下顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //2:设置instance指向刚分配的内存地址
ctorInstance(memory); //3:初始化对象
那么如果有两个线程A、B,当线程A执行完instance
,但是还未进行初始化时,此时instance
已经不再指向null
。如果此时线程B抢占到CPU资源,会跳过if(instance == null)
条件,直接返回一个未被初始化的对象。(解决办法看第三步)
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
3. 在instance
属性前增加volatile
关键字,可以防止指令重排。那么instance
对象的引用要么指向null
,要么指向一个初始化完毕的对象。具体原因在volatile一文中会说。
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}