【极客思考】设计模式:你确定你真的理解了单例模式吗?

原创
2020/05/27 16:09
阅读数 703
摘要: 单例模式是创建类型中常用的一种设计模式。该模式下的类有且仅有一个实例。

什么是单例模式?

说到单例模式,其实大家应该都不陌生,因为真的太常用了,应该所有开发者接触设计模式的第一个模式。那我这里一句话简单说下为何使用单例:如果你希望你的某个类只需要有一个实例对象,并且全局共享,那么你就使用单例。

我喜欢的单例模式实现

单例模式是创建类型中常用的一种设计模式。该模式下的类有且仅有一个实例。单例模式常见的实现有懒汉式、饿汉式这两种方式,但是在这里,我不想讨论这两种方式,因为常见所以没有讨论和需要思考的价值。
 
让我们来看看以下的几种方式的一些实现机制:

一、双重校验锁(DCL)

上代码:
开发、单例模式、线程、
DCL双重加锁的方式保证每次调用getSingleton方法的时候都是同步的。其实加锁大家都能理解,就是解决多线程同步的问题。但其实这里有个重点,就是这行代码:
private volatile static Singleton singleton

为什么要用volatile去修饰呢,这边从两个方面去说明:

1.如果不用volatile修饰会怎么样?
 
这看起来似乎也是行的通的,但是了解过编译器和程序指令的话就会知道那是不可靠的,具体原因如下:
 
  1. 编译器优化了程序指令,以加快cpu处理速度。
  2. 多核cpu会动态调整表指令顺序,以加快并行运算能力。
简单理解,那就是现在都0202年了,一台计算机cpu和内核都是好几个出现的,不在是那个单核的老时代了,所以java文件编译成字节码指令之后,你的编码逻辑确实是串行的,计算机也会根据范式把你编程的逻辑结果给你执行返回,但是具体到cpu去执行指令的时候,为了体现多核的优势,会对一些指令做并行处理,以加快程序运行速度。
 
我想好奇的你还是想知道,如果不加volatile的话,会在什么时候出现问题,那我给你说说问题出现的顺序:
 
  1. 线程A,调用方法获取实例,发现对象未实例化,准备开始实例化。
  2. 由于编译器优化了程序的指令,允许对象在构造函数未调用完成前,将共享变量的引用指向部分构造的对象,虽然对象未完全实例化,但是已经不为null了。
  3. 线程B进入也要调用方法获取实例,发现部分构造的对象已经不为null,则直接返回了该对象。
至于线程B返回之后会发生什么,可想而知,没实例化完,那么就会导致调用部分的方法的时候,就会有空指针的异常,所以就是我上面说的,不可靠。
 
2.volatile作用是啥?
 
为了解决这个问题,JDK1.6之后的版本提供了该关键字, 其实就是为了让其修饰的变量你能够在线程间可见,而所谓的可见,那就是大家都从主存中获取,至于主存等概念在这里就不展开说明了。
 
可以这么理解: 在线程B读取volatile变量后,线程A在写这个volatile之前,所有可见的共享变量的值都将立即变得对线程B可见。
对应上面的问题解决也就是:线程A在未初始化完,singleton变量那就是null,线程B读到的也就是null,那么当线程B再进去想要加锁实例化的时候,发现线程A获取了锁正在实例化,那就阻塞了起来,直到A实例化完释放锁,但是因为实例化完之后B立马又知道该变量不为null了所以在第二个判断的时候,就不用进去new了,返回了。

二、静态内部类

上代码:
静态内部类是一个我比较喜欢的实现方式,当然很明显代码少,逻辑较为简单。这种方式主要是利用了classloader机制来保证初始化singleton的时候只有一个线程,避免了需要再去保证线程同步的问题。同时我们把这种方式实例化有lazy loading的效果,其实主要是因为静态内部类Holder类并不会在Singleton类被装载的时候就被初始化了,只有当Holder类被主动使用,也就是调用了getSingleton方法之后,才会显示的装载Holder类,从而实例化singleton对象。如果singleton对象是一个消耗资源占用比较大的内存的对象的时候,如果你希望延迟加载的话,那么这种方式是个不错的选择。
 
但是其实静态内部类的方式实际上并没有想象中的那么完美,因为它无法阻挡反射和反序列攻击,你可以利用前面两种方式再去构造新的Singleton的实例,所以不是严格意义上的单例。

三、枚举

上代码:
这种方式是Josh Bloch提倡的,利用枚举的特性,让JVM来保证线程安全和单例的问题,还能防止反序列化和反射,除了大家不怎么常用外,其实这种简单的方式是个很好的方式。
 
反编译看一下,其实枚举是在static块中进行的对象的创建:

单例模式真的有那么好吗?

优点:
 
1.提供了唯一实例的受控访问。
2.因为只有一个实例,节约了系统资源,提高系统性能。
 
缺点:
 
1.单例模式没有抽象层,扩展比较困难。
2.单例类的职责过重,违背了“单一职责原则”。
 
我的推荐
 
我们去使用单例基本目标就是为了节省内存资源,而且一般的web项目都会引入Spring框架,通过Spring实现的单例和上面设计模式说的单例有所不同。设计模式的单例是在整个Java应用中只有一个实例,而Spring中的单例是在一个IOC容器中就只有一个单例。但对于web应用来说,web容器(Jetty或tomcat)对用户的每个请求都会创建一个单独的servlet线程去处理请求,Spring框架下的接口每个action也都是单例的,那么其实就保证了我们使用的是一个实例。
 
同时Spring也支持我们通过注解或者xml进行lazy-init,也可以指定scope确定其是否为全局单例,又或者是多个实例,对于程序来说有了更多的选择。
当然上面提到的线程安全的问题,其实大多数情况下Spring是没有去保证所有bean的线程安全,所以主动权交给了开发者,我们自己编写程序要保证线程安全的。不过在我们经常使用的数据库dao层的那些dao 的bean对象,Spring通过ThreadLocal对象,区别与我们常用的加锁的方式而是用空间换时间,给每个线程分配了独自的变量副本,从而隔离了多线程访问对数据访问的冲突,保证了线程安全性。至于这个类和这个机制,这里就不展开谈了,谈多了这篇文章就装不下了。
 
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部