单例的四大原则:
- 构造私有。
- 以静态方法或者枚举返回实例。
- 确保实例只有一个,尤其是多线程环境。
- 确保反序列换时不会重新构建对象。
主流的单例模式有DCL和静态内部类这两种懒加载方式
DCL(双重检查锁)
public class SingleTon {
private int mVal;
private static volatile SingleTon sInstance;
private SingleTon(int val) {
mVal = val;
}
public static SingleTon getInstance() {
if (sInstance == null) { //第一次判断
synchronized (SingleTon.class) {
if (sInstance == null) { //第二次判断
sInstance = new SingleTon(1);
}
}
}
return sInstance;
}
public void print() {
Log.e("SingleTon", "Val = " + mVal);
}
}
复制代码
双重检查锁存在两次判断sInstance是否为空,下面使用线程A和线程B来模拟运行,来说明两次判断的必要性。
-
线程A运行到第一次判断,返回true,这个时候线程B开始运行。
-
线程B也运行到第一次判断,因为sInstance还没有创建,因此为true,接着运行下面的代码,使用synchronized加锁,再运行到第二次判断,这个时候就算线程B停止,线程A运行,因为加锁的原因,线程A也是处于阻塞状态。还是要等到线程B释放锁,轮到线程B就开始创建对象,然后释放锁。
-
再轮到线程A,到了第二次判断,返回false,因此就不再创建对象。
DCL还有个关键点是使用了volatile,利用了volatile的可见性和禁止重排序的特性。
- 可见性解释
被volatile修饰的变量,一个线程对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。
线程A:将sInstance load到自己的工作内存中,此时sInstance为null;
切到线程B 对sInstance 进行了赋值,赋完值后,因为volatile原因,会马上刷新到主内存中
再次切到线程A:再次将sInstance load到自己的工作内存中,此时不为null
如果没有使用volatile,线程B可能来不及刷新sInstance到主内存;
二次判断时,线程A也不会重新去主内存读取。因此可见性发挥了作用。
- 禁止重排序的特性解释
sInstance = new SingleTon(1) 这行其实有三个步骤
- 在堆内存中申请了空间。
- 对mVal参数进行了赋值。
- 对象并指向堆内存空间。
如果不禁止重排序,会存在1、3、2的情况,当执行了1、3,还没执行2时,线程切换了,也就是对象已经创建了,sInstance == null 为false,但是val还没有赋值,别的线程可能会直接使用未赋值的mVal来使用,从而出现问题。因此禁止重排序发挥了作用。
内部静态类
public class SingleTon {
private int mVal;
private SingleTon(int val) {
mVal = val;
}
private static class SingleTonHolder {
private static final SingleTon INSTANCE = new SingleTon(2);
}
public static SingleTon getInstance() {
return SingleTonHolder.INSTANCE;
}
public void print() {
Log.e("SingleTon", "Val = " + mVal);
}
}
复制代码
深入理解内部静态类实现单例的原理,需要知道 类的加载过程(加载 校验 准备 解析 初始化)
类初始化方法为<clinit>()
,是为类变量设置初始值, 是一个类或接口被首次使用前的最后一项工作。
<clinit>()
被调用的时机:
类的初始化时机如下:
- 首次创建某个类的新实例时–new, 反射, 克隆 或 反序列化;
- 首次调用某个类的静态方法时;
- 首次使用某个类或接口的静态字段或对该字段(final 字段除外)赋值时;
- 首次调用java的某些反射方法时;
- 首次初始化某个类的子类时;
- 首次在虚拟机启动时某个含有 main() 方法的那个启动类
调用到getInstance()时候,会调用SingleTonHolder.INSTANCE,因为SingleTonHolder的静态字段INSTANCE被首次使用,所以SingleTonHolder的<clinit>()
方法被虚拟机调用,来给INSTANCE赋值。
虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。同一个加载器下,一个类型只会初始化一次。