饿汉模式
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
/**
* private : 如果不将构造方法的访问修饰限定符修改为 private 那么就会被构造出多个实例 就违背了单例模式的初衷
*/
private Singleton() {}
}
懒汉模式
class Singleton2 {
private volatile static Singleton2 instance;
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
private Singleton2() {}
}
总结
在多线程下,饿汉模式是线程安全的,懒汉模式是线程不安全的;
在这里我们再复习一下引起线程不安全的原因:
1.多线程下,抢占式执行(操作系统的随机调度)
2.多个线程修改同一个变量
3.修改操作不是原子的
4.内存可见性
5.指令重排序
public static Singleton2 getInstance() { if (instance == null) { instance = new Singleton2(); } return instance; }
懒汉模式:
当一个线程t1正在创建对象并赋值给instance,但并没有执行完时,另一个线程t2读到了还没有修改完的值,因此又重新创建了一个新的对象,此时就会导致实例被创建出多份儿了。
那么如何解决呢? - 加锁
public static Singleton2 getInstance() { synchronized (Singleton2.class) { if (instance == null) { instance = new Singleton2(); } } return instance; }
此时就不会出现上述问题了,但是又有新的问题出现了
仔细观察我们可以发现,我们只需要在第一次进行加锁操作,当时里被创建后便不需要了,直接返回即可,如果每次都需要加锁,那岂不是很烦。因此我们需要在进入加锁操作前再加上一层if语句,来判断一下是否需要创建实例。
public static Singleton2 getInstance() { if (instance == null) { synchronized (Singleton2.class) { if (instance == null) { instance = new Singleton2(); } } } return instance; }上述代码,还有一个非常重要的问题!
假设两个线程t1,t2调用getInstance方法,t1拿到锁,进入new操作,这里的new操作可以粗略的分成三个步骤:
1.申请内存,得到内存首地址
2.调用构造方法,初始化实例
3.将内存首地址赋值给instance
上述过程编译器可能会进行“指令重排序”的优化
如果是单线程,指令2和3发生调换是无所谓的
如果是多线程,就会出现问题。
假设t1线程拿到锁正在进行 new 操作 ,且发生指令重排序,先执行指令3,此时线程t2判断instance不为空,直接返回instance,那么我们知道,此时t1线程虽然把内存首地址赋值给了instance,但里面的数据是无效的,而t2线程拿到instance后,可能会对其进行解引用操作(使用里面的属性/方法),就会出现问题!
那么如何防止呢?也是需要用到 volatile 关键字 - 防止指令重排序
private volatile static Singleton2 instance;