单例模式

单例的四大原则:

  1. 构造私有。
  2. 以静态方法或者枚举返回实例。
  3. 确保实例只有一个,尤其是多线程环境。
  4. 确保反序列换时不会重新构建对象。

单例模式结构图.png

主流的单例模式有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来模拟运行,来说明两次判断的必要性。

  1. 线程A运行到第一次判断,返回true,这个时候线程B开始运行。

  2. 线程B也运行到第一次判断,因为sInstance还没有创建,因此为true,接着运行下面的代码,使用synchronized加锁,再运行到第二次判断,这个时候就算线程B停止,线程A运行,因为加锁的原因,线程A也是处于阻塞状态。还是要等到线程B释放锁,轮到线程B就开始创建对象,然后释放锁。

  3. 再轮到线程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) 这行其实有三个步骤

  1. 在堆内存中申请了空间。
  2. 对mVal参数进行了赋值。
  3. 对象并指向堆内存空间。

如果不禁止重排序,会存在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>()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。同一个加载器下,一个类型只会初始化一次。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享