单利模式

Start

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 方法的时候:

  1. Thread1 执行到第 8 行,开始进行对象的初始化。
  2. Thread2 执行到第 5 行,判断 singleton == null
  3. Thread2 经过判断发现 singleton != null,所以执行第 12 行,返回 singleton。
  4. 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

申明:开始和结束的图片来源网上,侵删

End

你可能感兴趣的:(单利模式)