那这种方式兼顾了性能和线程安全,而且也是懒加载的,那现在我们来创建一个类,这个类名也是懒加载的,双重检查
首先从上至下是一个时间,线程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);
}
}