DoubleCheck双重检查实战及原理解析

那这种方式兼顾了性能和线程安全,而且也是懒加载的,那现在我们来创建一个类,这个类名也是懒加载的,双重检查

首先从上至下是一个时间,线程0,因为刚刚呢,我们debug的时候,是从0开始的,所以现在演示的是一个单线程的情况,

注意1234这几个步骤,首先分配对象的内存空间,然后正常来说要初始化这个对象,然后设置instance指向内存空间,

这个instance就是我们代码里面lazyDoubleCheckSingleton,只所以没有命名成instance,是为了下载到源码之后,

很好的区分,所以我们每个单例模式里面,对象的命名都会和单例模式有关,然后初始访问对象,这个初次访问对象,

也就是线程0开始访问这个对象了,所以2和3怎么换顺序,4都是在最后,2和3的重排序,对结果并没有影响,但是重排序

并不是百分百命中的,是有一定概率的,但是这种隐患我们一定要消除,这个是单线程没有什么问题,看一下多线程模拟一下

首先时间还是从上至下,线程0从左侧划分,右侧是线程1,首先第一个时间点分配对象的内存空间,

假如线程0开始重排序了,先设置了instance指向了内存空间,这个时候线程1从上至下判断instance是否为null,

这个时候判断出来了,instance并不为Null,因为他有指向内存空间,然后线程1开始访问对象,也就是线程1比线程0

更早的访问对象,所以线程1访问到的对象呢,是一个在线程0中还没有被初始化成的对象,那这个时候就有问题了,

这个对象并没有被完整的初始化上,系统就要报异常了,那对于2和3的重排序刚刚也说了,并不影响线程0的第4个

步骤,访问了这个对象,我们知道了问题所在,我们怎么解决呢,我们可以不允许2和3重排序,或者允许线程0的2和3

重排序,但是不允许线程1看到这个重排序,那我们首先可以让2和3不允许重排序
package com.learn.design.pattern.creational.singleton;

/**
 * DoubleCheck关注的是什么呢
 * 双重检查
 * 在哪里检查
 * 
 * 
 * @author Leon.Sun
 *
 */
public class LazyDoubleCheckSingleton {
	/**
	 * 我们声明volatile
	 * 我们只要做这么一个小小的修改
	 * 就可以实现线程安全的延迟初始化
	 * 这样重排序就可以被禁止
	 * 那在多线程的时候呢
	 * CPU也有共享内存
	 * 我们在加了volatile关键字之后
	 * 所有线程都能够看到共享内存的执行状态
	 * 保证了内存的可见性
	 * 那这里面就和多线程有关了
	 * 关于volatile修饰的共享变量呢
	 * 在进行写操作的时候
	 * 会多出一些汇编代码
	 * 起到两个作用
	 * 第一是将当前处理器缓存好的数据缓存到数据内存
	 * 那这个写回到内存的操作呢
	 * 回写到其他内存缓存了
	 * 该内存的地址数据无效
	 * 那因为其他CPU内存数据无效了
	 * 所以他们又从共享内存共享数据
	 * 这样呢就保证了内存的可见性
	 * 这里面主要是用了缓存一致性协议
	 * 那当处理器发现我这个缓存已经无效了
	 * 所以我在进行操作的时候
	 * 会重新从系统内存中把数据读到处理器的缓存里
	 * 那我们就不深入讲解这块了
	 * 再讲就要到汇编和信号的问题了
	 * 我们的重点还是单例模式
	 * 通过volatile和doublecheck这种方式呢
	 * 既兼顾了性能又兼顾了线程
	 * 
	 */
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    /**
     * 首先我们的方法不用锁了
     * 也就是说public static LazyDoubleCheckSingleton getInstance()这个方法一调到这里
     * 就立刻锁上
     * 而是把锁定放在方法体中
     * 对象还是进行一个判断
     * 判断完成之后
     * 这个时候呢
     * 
     * Thread0在if(lazyDoubleCheckSingleton == null)这里
     * 这个instance是null
     * 我们切到Thread1上
     * Thread1进入if
     * 第二重判断
     * 我们重点关注DoubleCheck
     * 我们再切换到Thread0
     * 因为lazyDoubleCheckSingleton为空
     * 所以Thread0也可以进来
     * 但是在synchronized (LazyDoubleCheckSingleton.class)会被block掉
     * 那我们再切换到Thread1上
     * Thread1单步走
     * 开始new
     * 这个时候已经new完了
     * Thread1现在释放了这个锁
     * 所以切回到Thread0
     * Thread0获取这个锁之后
     * 进入第二个if判断
     * 这个时候进入第二层的空判断
     * 那他判断lazyDoubleCheckSingleton这个对象并不为空
     * 所以他直接走到这里
     * 然后在单步走
     * 走到return
     * 注意他后面是425
     * 而Thread1也是425
     * 因为是debug
     * 执行非常快
     * 因为我们通过volitale关键字已经去new对象的时候
     * 有可能出现的重排序已经解决了
     * 那我们直接F6单步走
     * 现在我们看一下console
     * 并且在创建对象的时候
     * 希望能理解doublecheck
     * 单例模式的一个演进
     * 一定要学会多线程debug的实战技能
     * 非常重要
     * 那刚刚我们也说了
     * 对这种重排序
     * 我们有两种解决方案
     * 第一是不允许2和3重排序
     * 还有一种方案允许23重排序
     * 但是不允许其他线程看到这个重排序
     * 刚刚我们是基于不允许23重排序来解决的
     * 接下来我们使用第二种方式来解决这个问题
     * 同时讲解一下原理
     * 
     * 
     * 
     * 
     * 
     * @return
     */
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
        	/**
        	 * 判断完成之后我们锁定这个单例的这个类
        	 * synchronized (LazyDoubleCheckSingleton.class)
        	 * 注意这里
        	 * 我们现在锁定了这个类
        	 * 那也就代表着if是进来的
        	 * 至少进到这个里面的线程到if(lazyDoubleCheckSingleton == null)这里的时候
        	 * 这个对象还是空的
        	 * 那我们想象一下
        	 * 因为if(lazyDoubleCheckSingleton == null)这里没有锁
        	 * 所以如果另外一个线程进来
        	 * 如果判断if(lazyDoubleCheckSingleton == null)它为空
        	 * 那到synchronized (LazyDoubleCheckSingleton.class)
        	 * 也会阻塞
        	 * 如果进入到这里面的
        	 * 已经把这个对象生成好了
        	 * 那刚刚新进来的线程呢
        	 * if(lazyDoubleCheckSingleton == null)判断的时候
        	 * 会直接return
        	 * 这里面还有一个小坑
        	 * 就是指定重排序的问题
        	 * 那一会讲到这里再说
        	 * 加锁之后我们肯定还要做一层空的判断
        	 * 这个lazyDoubleCheckSingleton对象如果为null的话
        	 * 这个时候我才会给他赋值
        	 * lazyDoubleCheckSingleton这个对象我们平时使用的时候
        	 * 一般命名成instance
        	 * 只不过我们这里要讲多种方式
        	 * 通过命名也加以区分
        	 * 免得弄混了
        	 * 这个instance就是单例的对象
        	 * 那我们看一下现在的这种写法
        	 * synchronized (LazyDoubleCheckSingleton.class)不加锁
        	 * 不为null就直接返回
        	 * 如果为null的话也只有一个线程进入到这里面
        	 * 这个就可以大量的减少synchronized加载方法上的时候带来的性能开销
        	 * 看上去我们的这个实现非常完美
        	 * 多个线程的时候我们通过加锁来保证只有 一个线程来创建对象
        	 * 当对象创建好之后呢
        	 * 以后再调用getInstance方法的时候
        	 * 都不会再需要加锁
        	 * 直接返回已创建好的对象
        	 * 这里面有个隐患
        	 * 那隐患出在上面的if判断
        	 * 还有lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
        	 * 现在说一下为什么
        	 * 首先在上面的if判断的时候
        	 * 虽然判断了这个对象是不是为空
        	 * 这个时候是有可能不为空的
        	 * 虽然他不为空
        	 * 但是这个对象可能还没有完成初始化
        	 * 也就是lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();还没有执行完成
        	 * 那我们来看一下这个new的这块代码
        	 * 当我把这个对象new出来的时候
        	 * 看上去是一行
        	 * 实际上这里面经历了三个步骤
        	 * 我们加一个注释写到这里吧
        	 * 第一步分配内存给这个对象
        	 * 也就是给这个对象分配内存
        	 * 第二步初始化这个对象
        	 * 第三步设置lazyDoubleCheckSingleton指向刚分配的内存地址
        	 * 也就是这一行执行了三个操作
        	 * 那在2和3的时候
        	 * 可能会被重排序
        	 * 也就是说呢
        	 * 2和3的顺序有可能会被颠倒
        	 * 变成这样的
        	 * 先分配这个内存给这个对象
        	 * 然后单例对象指向刚分配的内存地址
        	 * 注意现在已经指向了这个内存地址
        	 * 所以这里空判断的时候呢
        	 * 并不为空
        	 * 但是这个单例对象有可能没有初始化完成
        	 * 这里面就要说一下
        	 * JAVA语言规范里面有说
        	 * 所有线程在执行JAVA程序时
        	 * 必须遵守intra-thread semantics这么一个约定
        	 * 他保证重排序不会改变单线程内的程序执行结果
        	 * 例如123这几个执行步骤
        	 * 对于单线程来说
        	 * 2和3互换位置
        	 * 其实不会改变单线程的执行结果
        	 * 所以JAVA语言规范允许那些在单线程内不会改变单线程执行结果的重排序
        	 * 也就是说2和3是允许的
        	 * 因为这个重排序可以提高程序的执行性能
        	 * 为了更好地理解呢
        	 * 画了一个图给大家看一下
        	 * 
        	 * 
        	 * 
        	 * 
        	 */
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    //1.分配内存给这个对象
//                  //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                    //2.初始化对象
//                    intra-thread semantics
//                    ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
package com.learn.design.pattern.creational.singleton;

public class T implements Runnable {
    @Override
    public void run() {
//        LazySingleton lazySingleton = LazySingleton.getInstance();
//        System.out.println(Thread.currentThread().getName()+"  "+lazySingleton);
    	/**
    	 * 调用他的getInstance方法
    	 * 
    	 */
        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
//        StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();;

//        ContainerSingleton.putInstance("object",new Object());
//        Object instance = ContainerSingleton.getInstance("object");
//        ThreadLocalInstance instance = ThreadLocalInstance.getInstance();

        System.out.println(Thread.currentThread().getName()+"  "+instance);

    }
}
package com.learn.design.pattern.creational.singleton;

public class T implements Runnable {
    @Override
    public void run() {
//        LazySingleton lazySingleton = LazySingleton.getInstance();
//        System.out.println(Thread.currentThread().getName()+"  "+lazySingleton);
    	/**
    	 * 调用他的getInstance方法
    	 * 
    	 */
        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
//        StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();;

//        ContainerSingleton.putInstance("object",new Object());
//        Object instance = ContainerSingleton.getInstance("object");
//        ThreadLocalInstance instance = ThreadLocalInstance.getInstance();

        System.out.println(Thread.currentThread().getName()+"  "+instance);

    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值