单例模式
应用场景:如果这个类没有自己的状态也就是实例化多个其实都是一样的,甚至实例化多个会造成程序错误或者增加维护复杂度
好处:这种情况下,设计成单例模式会节省内存资源,减少不必要的gc开销,保证程序正常运行
- 原始模式(不考虑并发):
示例如下
1.静态实例,静态属性在类中是唯一的
2.私有构造方法,防止实例化多个实例
3.公有的静态方法获取一个静态实例,方法必须是静态的因为是在未获取实例的情况下返回一个静态实例,如果是非静态方法,必须有实例才能调用
4.如果静态实例为null,则创建,否则直接返回
存在问题:多个线程同时获取实例为空,会创造多个实例
- 并发模式:
示例如下
虽然解决了并发情况下非单例的问题,但当一个线程访问这个同步方法时,其他线程必须等待挂起,这在并发场景下会造成很多无谓的等待
- 标准模式(双重加锁)
示例如下
由于需要同步的部分,只是实例还未创建的时候,实例创建之后,无需进行同步控制
这种方法,只在实例还未创建时,才进行等待,减少了无谓的等待。
同步块中的判空的必要性,当实例未创建时,两个线程同时进入外面的singletonDemo==null块,一个线程获得了线程锁,进入同步块创建了实例,返回释放了同步锁,另一个线程获得线程锁,如果没有判断,则直接创建了实例,导致出现多个实例的情况,这不是我们所期望的。
上述在代码层面看起来没有问题,但在jvm层面由于创建对象的过程不是原子操作,所以有可能返回一个还没有初始化完成的对象
由于对象的创建过程不是原子操作(详见https://blog.csdn.net/cindyxue_17/article/details/114263117)简单来说包括 分配内存空间->变量初始化->执行构造函数->返回地址给引用
在这个过程中,如果发生指令的重排序,则会返回一个只分配了地址但是还没有实例化完成的对象引用,在多线程场景下,其他线程会拿到一个没有初始化完成的对象,存在安全隐患,针对该问题存在以下几种解决方案:
-
虚拟机层面的优化1:
示例如下(使用内部类作为单例)
一个类的静态属性只在类第一次加载时初始化,在初始化一半的时候别的线程是无法访问的,jvm会对这个过程进行同步,保证并发下的单例 -
虚拟机层面优化2:‘
示例如下(不适用内部类作为单例)
这两种方式可以保证在并发情况下,不会由于初始化未完成而获取错误的实例 -
懒汉模式
示例如下,是一种比较简单的单例模式,但会存在,如果该类的其他静态属性被访问,也会初始化这个单例实例,但实际上这个单例实例可能从来不会用到,造成浪费
示例如下 -
双重锁定模式的优化(volatile不推荐)
如上述双重加锁模式,由于jvm虚拟机的指令优化,会存在未正确初始化实例的情况,所以可以给静态的单例实例属性加上关键字volatile,(volatile禁止了JVM自动的指令重排序优化,并且强行保证线程中对变量所做的任何写入操作对其他线程都是即时可见的。总之volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作)。
示例如下: