这样看起来似乎实现了单例模式,但是它目前是线程不安全的。
图片.png
解决办法:
图片.png
加在静态方法上相当于锁是这个类的class文件,如果加在普通成员方法上,相当于锁的是堆内存中的生成对象
图片.png
但是同步锁会比较消耗资源,这里边有加锁和解锁的开销,而且synchronized修饰static方法时,锁的是这个class,锁的范围较大,对性能比较有影响。
而且现在还存在一个问题,就是当第一个实例创建出来后,此时应该是不会加锁,判断非空直接返回实例。但是它依旧会先给类加上锁,返回实例出去再解锁。这过程中,在已经有实例的前提情况下依旧加锁就是白白浪费计算机资源了。
DoubleCheck兼顾了性能和线程安全
为了解决已经有了实例还会加锁这个问题,先判断是否已经有实例再决定是否加锁,已经有实例就不加锁,直接返回实例出去了。
但这里面依旧有隐患
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一行看似只有一行,但实际上经历了3个步骤。1.分配内存给这个对象
2.初始化对象
3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
这3个步骤的第2步骤,第3步骤有可能会被颠倒。
对于单线程,第2步骤,第3步骤被颠倒不会影响执行结果。但对于多线程就不一定了。
图片.png
上述情况中,线程1比线程0更早地访问对象,线程1访问到的对象是在线程0中还没有初始化完成的一个对象,系统就会报异常。
解决办法:不允许2和3重排序或者允许重排序,但不允许别的线程看见重排序
在变量前加volatile后该变量在new对象时重排序就会被禁止,线程就安全了
此时步骤2和步骤3严格按先后顺序执行。
允许重排序,但不允许别的线程看见重排序
通过静态内部类(本质是基于类的初始化)来解决。
jvm在类的初始化阶段,也就是class文件被加载后并且被线程使用之前都是类的初始化阶段,在这个阶段会执行类的初始化,在执行类的初始化期间呢,jvm回去获取一个锁,这个锁可以同步多个线程对一个类的初始化,也就是蓝色的部分,基于这个特性,我们可以实现基于静态内部类的并且线程安全的的延迟初始化方案。
一个类(泛指,接口也算)会被立刻初始化的五种情况
1、A类型实例被实现
2、A类型的静态方法被调用
3、A类中的静态成员被赋值
4、A类中的一个静态成员并且这个成员不是一个常量成员。
5、A类是一个顶级类
图片.png
图片.png
静态内部类这种方式的核心在于InnerClass这个类的Class对象初始化锁看哪个线程拿到。
拓展:
1、编译器优化的重排序,在不改变单线程语义的情况下重新安排语句的执行顺序
2、指指令级并行重排序,处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性将会改变语句对应机器指令的执行顺序
出自:https://blog.csdn.net/qq_41174684/article/details/90631746
网友评论