理解单例模式
发布于2022-06-28 22:29:42,更新于2022-07-23 01:57:31,标签:java 文章会持续修订,转载请注明来源地址:https://meethigher.top/blog最近,习大大要调研光谷企业,所以我也就调休了,然后总结下最近用到的单例!
说起来可笑,去年3月份面试时提到的DCL,不会。工作一年后,才略懂。纸上谈兵终觉浅,绝知此事要躬行!
单例模式,网上说是最简单的设计模式。
单例,顾名思义,单个实例,属于创建型模式,通过静态变量存储唯一的对象实例。我们new的对象,每new一个地址都不同,这叫做多例。
单例模式举个应用场景是,一个专用加解密对象,在创建时,通过一个随机密钥生成实例,我要保证所有使用的人,都拿到同一个加解密实例,才能解密别人加密过的。
下面举例单例模式的实现。
1 | public class Singleton { |
这是最经典的单例模式写法,在普通环境中运行良好。但是在高并发环境中,可能会出现创建出多个对象实例的现象。参见如下测试代码:
1 | public static void main(String[] args) { |
出现如上情况的原因是:多个线程同时调用getInstance方法时,判断singleton==null时,都满足条件,因此会创建多个实例对象,这也叫做线程不安全问题。
一、如何保证单例
解决办法
- 加锁。给getInstance方法增加synchronized同步锁。但是,这种做法,在高并发下,虽然保证了只拿到一个实例对象,但是也让后续的并发拿取耗费了不少性能。
- 懒汉模式
- 恶汉模式
1.1 懒汉模式-懒加载
顾名思义,他是一个懒汉,他不愿意动弹。什么时候需要吃饭了,他就什么时候开始想办法搞点食物。
即懒汉式一开始不会实例化,什么时候用就什么时候new,才进行实例化。
懒汉模式,有两种可选方式。
- 双检锁
- 延迟初始化占位类
双检锁
通过双重校验锁(double-checked-locking),即双检锁(DCL)。既保证了不影响并发调用的性能,又确保了并发时只会被创建一次。
1 | public class Singleton { |
延迟初始化占位类
延迟初始化占位类,算是对双检锁的一个优化,能做到同样的效果,主要是从理解程度上,更易于理解!
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段。这也是俗称整个类加载的生命周期。
类加载与类加载生命周期,不是一回事。前者是后者的一部分。
加载阶段,书中没有明确提及。也正因为这个不明确,导致之后做开发时碰到了Bug。
但是类的初始化阶段,是有严格规范的。其中,调用静态方法、静态字段时,就会触发初始化。初始化完成之后才能被使用,此处就做到了类似于同步锁的作用。
类的初始化与对象的实例化也需要分清。
new 对象可以触发该类的初始化和对象的实例化。
仅调用静态方法,只会触发类的初始化。
参考自《深入理解Java虚拟机》
1 | public class Singleton { |
1.2 饿汉模式-急加载
顾名思义,他是一个饿汉,他很勤快就怕自己饿着。他总是先把食物准备好,什么时候需要吃了,他随时拿来吃,不需要临时去搞食物。
即饿汉式在类加载的时候同时对静态字段进行了初始化,并且创建单例对象,以后只管用即可。
类加载的时候,线程还未被创建。不会存在线程安全问题。
1 | public class Singleton { |
1.3 区别
效率:饿汉模式没有加任何的锁,因此执行效率高。懒汉模式一般会使用锁,效率相比饿汉会差一点,不过也只是在于高并发创建的时候,只要对象创建了,其实也没差别了。
空间:饿汉模式在一开始类加载就实例化,无论是否使用,都会实例化,所以会占据空间,占用内存。而懒汉式是按需加载。
二、致谢
参考