并发编程:一次搞定单例模式
模式是脱离语言的。
一、单例的模式由来
多线程要操作同一个对象,保证对象的唯一性。
如何解决?
实例化过程只实例化一次。
单例模式的四大原则
1.构造方法私有化 2.以静态方法或者枚举返回实例 3.确保实例只有一个,尤其是多线程环境 4.确保反序列化时,不会重新构建对象
我们常见的单例模式有:
饿汉模式
懒汉模式
双重检索模式
静态内部类模式
枚举模式
二、单例模式的分类
1.饿汉模式
饿汉模式安全性:
在 HungrySingleton 类被类加载器加载的时候已经被实例化,所有只有这一次,以空间换时间,线程是安全的。
效率问题
没有延迟加载,如果创建后不被使用,占内存,影响性能
2.懒汉式
懒汉模式安全性:LazySingleton 是在方法被调用后才创建对象,用时间换空间,在多线程环境下存在风险。
3.双重检索懒汉模式(Double -Check -Lock)
上面的代码:在获取实例对象 getInstance()的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 DCLSingleton.class,并再次检查 instance 是否为空,如果还为空则创建 DCLSingleton 的一个实例。
我们假如有两个线程 A,B 同时调用 getInstance()方法,他们会同时发现 instance ==null,于是同时对 DCLSingleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假如是线程 A),另外一个线程会处于等待状态(假如是线程 B);线程 A 会创建一个 DCLSingleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 Objects.isNull(instance)时会发现,已经创建过 DCLSingleton 实例了,所以线程 B 不会再创建一个 DCLSingleton 实例。
这看上去一切都很完美,无懈可击,但实际上这个 getInstance()方法并不完美,问题出在哪里呢,出在 new 操作上,我们以为的 new 操作应该是:
分配一块内存 M
在内存 M 上初始化 DCLSingleton 对象;
然后 M 的地址赋值给 instance 变量
但可能经过 JVM 优化过后的执行顺序是这样的:
1.分配一个内存 M;
2.将 M 的地址赋值 instance 变量
3.最后再内存上初始化 DCLSingleton 对象。
优化后会导致什么问题呢? 我们假设线程 A 先执行 getInstance()方法,当执行完指令 2 时恰好发生了线程切换,切换了线程上 B 上;如果此时线程 B 也执行 getInstance()方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能会触发空指针异常。
4.双重检索(DCL)+ volatile
volatile 的作用:
1.保证可见性:
对共享变量的修改,其它线程马上能感知到 ,这样 volatile 可以确保 instance 每次都会在主内存中读取,保证 instance 的一致性
2.保证有序性:
重排序:(编译阶段,指令优化阶段会进行重排序)as-if-serial:重排序后在单线程不影响程序的执行结果,对多线程有影响 volatile 原则:volatile 之前的代码不能调整到它的后面 volatile 之后的代码不能调整到它的前面位置不变化
5.静态内部类模式
静态内部类的优点:
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 instance,故而不占用内存,即当 Singleton 第一次被加载时,并不需要加载 SingleHolder,只有当 getInstance()方法第一次被调用时,才会去初始化 instance,第一次调用 getInstance()方法会导致 JVM 加载 SingletonHolder 类,这种方法不仅能保证线程安全,也能保证单例的唯一性,同时也延迟了到单例的实例化。
补充知识
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
遇到 new、getstatic、setstatic 或者 invokestatic 这 4 个字节码指令时,对应的 java 代码场景为:new 一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final 修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的类),虚拟机会先初始化这个类。
使用 JDK 1.7 等动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这 5 种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如 Context 这种参数,所以,我们创建单例时,可以在静态内部类与 DCL 模式里自己斟酌。
评论