1. 懒汉式
public class Singleton {
private Context mContext;
private static final Object mLock = new Object();
private static Singleton mInstance;
public static Singleton getInstance(Context context) {
synchronized (mLock) {
if (mInstance == null) {
mInstance = new Singleton(context);
}
return mInstance;
}
}
private Singleton(Context context) {
this.mContext = context.getApplicationContext();
}
}
2. 双重校验锁
public class Singleton {
private static Singleton singletonSync;
private Singleton () {}
public static Singleton getSingletonSync() {
if (singletonSync == null) {
synchronized (Singleton.class) {
if (singletonSync == null) {
singletonSync = new Singleton();
}
}
}
return singletonSync;
}
}
以上代码,我们通过使用 synchronized 对 Singleton.class 进行加锁,可以保证同一时间只有一个线程可以执行到同步代码块中的内容,也就是说 singleton = new Singleton() 这个操作只会执行一次,这就是实现了一个单例。
但是,当我们在代码中使用上述单例对象的时候有可能发生空指针异常。这是一个比较诡异的情况。
我们假设 Thread1 和 Thread2 两个线程同时请求 Singleton. getSingletonSync 方法的时候:
- Thread1 执行到第 8 行,开始进行对象的初始化。
- Thread2 执行到第 5 行,判断
singleton == null
。 - Thread2 经过判断发现
singleton != null
,所以执行第 12 行,返回 singleton。 - Thread2 拿到 singleton 对象之后,开始执行后续的操作,比如调用 singleton.call()。
以上过程,看上去并没有什么问题,但是,其实,在 Step4,Thread2 在调用 singleton.call() 的时候,是有可能抛出空指针异常的。
之所有会有 NPE 抛出,是因为在 Step3,Thread2 拿到的 singleton 对象并不是一个完整的对象。
我们这里来分析一下,singleton = new Singleton();
这行代码到底做了什么事情,大致过程如下:
1、虚拟机遇到 new 指令,到常量池定位到这个类的符号引用。
2、检查符号引用代表的类是否被加载、解析、初始化过。
3、虚拟机为对象分配内存。
4、虚拟机将分配到的内存空间都初始化为零值。
5、虚拟机对对象进行必要的设置。
6、执行方法,成员变量进行初始化。
7、将对象的引用指向这个内存区域。
我们把这个过程简化一下,简化成3个步骤:
a、JVM 为对象分配一块内存 M
b、在内存 M 上为对象进行初始化
c、将内存 M 的地址复制给 singleton 变量
因为将内存的地址赋值给 singleton 变量是最后一步,所以 Thread1 在这一步骤执行之前,Thread2 在对 singleton==null
进行判断一直都是 true
的,那么他会一直阻塞,直到 Thread1 将这一步骤执行完。
但是,以上过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排成:
a、JVM为对象分配一块内存M
c、将内存的地址复制给singleton变量
b、在内存M上为对象进行初始化
这样的话,Thread1 会先执行内存分配,在执行变量赋值,最后执行对象的初始化,那么,也就是说,在Thread1 还没有为对象进行初始化的时候,Thread2 进来判断 singleton==null
就可能提前得到一个 false
,则会返回一个不完整的 sigleton 对象,因为他还未完成初始化操作。
这种情况一旦发生,我们拿到了一个不完整的 singleton 对象,当尝试使用这个对象的时候就极有可能发生 NPE 异常。
那么,怎么解决这个问题呢?
因为指令重排导致了这个问题,那就避免指令重排就行了。
所以,volatile 就派上用场了,因为 volatile 可以避免指令重排。只要将代码改成以下代码,就可以解决这个问题:
public class Singleton {
/**
* 双重校验锁无NPE版本
* @return
*/
private volatile static Singleton singletonDoubleSync;
public Singleton getSingletonDoubleSync() {
if (singletonDoubleSync == null) {
synchronized (Singleton.class) {
if (singletonDoubleSync == null) {
singletonDoubleSync = new Singleton();
}
}
}
return singletonDoubleSync;
}
}
对 singleton 使用 volatile 约束,保证他的初始化过程不会被指令重排。
3. 静态内部类模式
public class Singleton {
private Singleton(){}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类的原理是:
当 SingleTon 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance() 方法第一次被调用时,才会去初始化 INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance() 方法并没有多次去 new 对象,取的都是同一个 INSTANCE 对象。
虚拟机会保证一个类的
缺点在于无法传递参数,如Context等。
4. 饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
参考文献 : https://juejin.im/post/5d5c9fbce51d4561cd246641
申明:开始和结束的图片来源网上,侵删