双检查锁下的单例懒汉模式
1、什么是单例懒汉模式?
单例模式:保证一个类在JVM中只有一个实例,并提供一个全局访问点。
懒汉模式:在第一次调用获取实例方法时才创建对象,而不是类加载时。核心优点是延迟加载,节省资源;缺点是线程不安全。
举个例子:
publicclassSingleton{privatestaticSingletoninstance1;publicstaticSingletongetInstance1(){if(instance1==null){instance1=newSingleton();}returninstance1;}对象实例创建流程:
- 先分配一片足够放下Singleton对象的空间,赋默认值。
- 再执行构造方法初始化对象。
- 建立关联,对象引用指向该对象实例。
2、单例懒汉模式下的问题
问题:CPU或者编译器会将第2、3步的指令重排序,单线程下不会有问题。多线程下问题有两个:
T1 如果先执行3,还没执行2的时候,T2 判断得知对象引用不为空,直接返回了未初始化的对象,导致错误。
会导致重复创建,违背单例模式。如果T1在创建对象的过程中,另一个线程T2判断得知instance1==null,那么它会进入对象创建流程,从而创建重复对象并替换引用。
如何解决:
- 错误方案一:直接给instance1标记为volatile,然后不做任何处理。问题:由于它无法确保原子性,因此创建过程会受其他线程影响,从而导致重复创建。
- 错误方案二:双重检查锁,问题:拿到未初始化对象。具体见下文:
3、双检查锁失效
首先看一下代码:
publicclassSingleton{privatestaticSingletoninstance2;publicstaticSingletongetInstance2(){if(instance2==null){synchronized(Singleton.class){if(instance2==null){instance2=newSingleton();}}}returninstance2;}}双检查锁:对象为空时,只让一个线程进去初始化对象。另一个等待线程获取锁后,再次判断是否被初始化,避免重复创建。
为什么能避免重复创建?并发调用getInstance2方法的情况下,后一个线程判断为空之后,还需要等待锁,而那个锁被释放之后。锁内部会再次检查该对象是否被创建,如果已经被创建,则直接跳过对象的创建过程,从而避免再次创建。
问题:只解决了重复创建的问题。还是会出现它被引用,但是没有调用构造方法初始化时。另一个线程通过了非空判断,直接拿到一个未初始化对象的情况。
4、volatile作用
volatile 保证变量的可见性和禁止指令重排序,但不保证原子性。
可见性:写 volatile 会立即刷新到主内存,读 volatile 从主内存读。
重排序:修饰的变量前后插入内存屏障,防止指令重排。
原子性:volatile不能保证i++这类复合操作的原子性,因为读、改、写三步可能被其他线程影响。
因此这里直接利用它的禁止指令重排序的作用,直接修饰单例对象,这样就可以配合synchronized同时保证创建对象过程的原子性、禁止创建过程的指令重排序。
volatileprivatestaticSingletoninstance2;