作者
:学Java的冬瓜
博客主页
:☀冬瓜的主页
专栏
:【JavaEE】
分享
:久闻中原歧王战力无双,今日一见,果非虚言!——《画江湖之不良人》
主要内容
:单例模式,饿汉模式和懒汉模式的写法,懒汉模式的深度分析,怎么解决懒汉模式读写非原子,内存可见性以及指令重排序等问题。
三招:实例私有化、构造方法私有化、只提供getInstance静态方法供外部类使用。
package demo;
// 单例模式
// 饿汉式
class Singleton {
// 实例私有化
private static Singleton instance = new Singleton();
// 构造方法私有化
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
public class Demo1 {
public static void main(String[] args) {
// 因为Singleton类构造方法私有,所以不能在Singleton类外new,而只能调用getInstance方法
// Singleton对象在类加载时就已经创建了,程序员不能再去手动创建,这样就实现了单例,即只有一个实例Singleton对象
//demo.Singleton s = new demo.Singleton(); // 报错
Singleton s = Singleton.getInstance();
}
}
按照饿汉模式代码的思路,我们写出的懒汉模式的代码如下,但是,下面的代码有问题,什么问题呢?让我们接着往下看
class SingletonLazy {
private static SingletonLazy instence = null;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
if(instence == null){
instence = new SingletonLazy();
}
return instence;
}
}
我们来看看 if(instance == null)这个条件,当多个线程同时进入if,那么这些线程把instance的值拷贝到寄存器上的值都为null,然后寄存器的值和null比较,这个判断就是true,那这些线程都会去创建SingletonLazy对象,那么单例模式就失效了。为什么会创建多个对象呢?其实就是instance的读和写不是原子性导致问题,所以我们就给它加锁,变成如下这样。
这个改造后的代码还存在问题,你想一下,每次在其它类里使用SingletonLazy s = SingletonLazy.getInstance();
获取单例的时候,如果是上面的代码,每次都会加锁,然后再判断。实际上,我们只需要第一次创建实例的时候加锁就行了,因为除去第一次后就只涉及到读,而不涉及到写,所以就不需要加锁以保证效率问题。所以我们就再在加锁外面给一个判断。
代码写到这里你以为没有问题了?不不不,还有问题!!!
我们假设一个可能存在的场景:此时有很多线程,都去执行getInstance,而此时这些线程都执行到了外层的if这里,然后有一个线程可以获取到锁,然后去内部执行。那么问题来了,这个时候如果内部的if(instance == null)
被第一个线程使用时,先把instance的值load到寄存器(或者cache)上,然后比较,然后创建单例。但是当第一个线程释放锁,后续的线程获取到锁进入后,如果不从内存把instance的值加载到寄存器,而是直接从寄存器读那后续线程读到的仍是null,那就又会去创建单例,导致单例失效。这是内存可见性导致单例失效(可以看看之前的博客线程安全里面详细讲了内存可见性问题)。
接下来还有指令重排序可能导致单例失效。我们先来了解:instance = new SingletonLazy
具体的操作可分为三步:1> 申请内存空间 2> 调用构造方法,把这个内存空间初始化为一个合理的对象 3> 把内存空间的引用给instance对象,如果第一次SingletonLazy s = SingletonLazy.getInstance();
创建单例对象时,是1、2、3的顺序执行,那么没有问题。那现在我们又来假设一个可能存在的场景:t1和t2线程,第一次执行getInstance创建单例对象时,t1先执行完1、3,然后被切出CPU(此时t1释放了锁),t2来执行。但是 t2看到t1执行完 3后,就认为instance非空了,就直接返回instance,且t2还可能会使用引用中的属性。但是由于t1还没有执行2操作,所以此时t2拿到这个instance是一个不完整对象,那就可能会出问题。
那我们从上面的两个分析里知道,上面经过改造的代码还可能存在内存可见性和指令重排序问题,那很明显,就是用volatile解决就行了。改造代码如下:
懒汉模式除了三招:实例私有化、构造方法私有化、只提供getInstance静态方法供外部类使用外,还要考虑很多问题:
1> synchronized加锁
2> 双层if判断各自作用
3> volatile解决内存可见性和指令重排序问题
package demo;
// 单例模式
// 懒汉模式
class SingletonLazy {
// volatile 解决内存可见性和指令重排序问题
private volatile static SingletonLazy instence = null;
private SingletonLazy(){}
public static SingletonLazy getInstance(){
// 外面的这层if用于判断,如没有这一层判断,则每次进来都会加锁,消耗大量时间做无用功
if(instence == null){
// synchronized保证instance的读和写是原子性的
synchronized (Singleton.class){
// 判断单例是否已经创建,若无,则创建。
if(instence == null){
instence = new SingletonLazy();
}
}
}
return instence;
}
}