常见的单例模式
本文仅讲解单例模式中的饿汉式和懒汉式(双检索)
文章目录
前言
先来简单的说说什么是单例模式。所谓单例就是在系统中只有一个该类的实例,或者说类只能被创建一次。单例模式(Singleton),也是23种设计模式中常见的一种。
一、单例模式的优缺点
优点:
1、在单例模式中,单例对象的类必须保证只有一个实例存在,这就说明这种模式能一定程度的减少内存开支。
2、配置的读取,现在市场上较为常见的场景,将配置信息放在某个配置文件中,这些配置数据由一个单例对象统一读取。采用单例模式,即可减少系统的性能开销。
缺点:
1、单例模式既只能被实例化一次,没有抽象层,那么它想要扩展,就没有其他的途径(代码无bug的情况下,下文会提到)
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
二、详解懒汉式、饿汉式
1.饿汉式
顾名思义,饿汉式,饿极了的汉子,有饭就吃,不会规规矩矩的等到饭点才去食堂,所以在加载初始化的时候就已经被创建出实例了。
先上代码(饿汉式):
import java.io.Serializable;
/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton {
private static final Singleton INST = new Singleton();
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试饿汉式的方法");
}
}
饿汉式测试
public class EhanTest {
public static void main(String[] args) {
Singleton.testfun();
//可以根据输出的对象判断是否是同一实例
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}
}
读完了代码,那么上述代码是否真的只能被创建一次呢?
其实不然,接着往下看:
(1)、反射机制
Class<?> clazz = Singleton.class;
//根据类型获取类的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructor();
//让私有的构造方法可以被使用
constructor.setAccessible(true);
//创建个新对象
constructor.newInstance();
既然根据反射机制可以破坏我的单例模式,那我能否预防这种场景发生呢?当然可以
import java.io.Serializable;
/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton {
private static final Singleton INST = new Singleton();
//构造方法
private Singleton(){
if (INST != null){
throw new RuntimeException("我已心有所属,不能再次被创建");
}
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试饿汉式的方法");
}
}
在构造方法中直接判断是否已经被创建即可
(2)、反序列化
这种场景需要你的单例模式实现序列化接口,还是较为常见
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(os);
//对象转换为字节流
obj.write(singleton);
ObjectInputStream bs = new ObjectInputStream(new ByteArrayInputStream(os.toByteArray()));
//还原对象
bs.readObject();
- 参考第一种反射机制,这种场景是否在构造方法中加入判断即可预防?实际上反序列化场景是不会走构造方法的
public Object readResolve(){
return INST;
}
- 预防这种情况加入readResolve()方法即可,原理比较麻烦,之后会写篇专门的叙述。
3、Unsafe()方法
unsafe是JDK提供的类,可以根据类型来创建实例,并且和反序列化相同,都是不走构造方法,这种场景目前不知道有什么方法可以预防,有了解的大神可以评论下,我也学习学习,哈哈哈
2.懒汉式(双检锁)
- 懒汉式,很懒的抠脚大汉,懒的一动不动,只有需要他的时候才会有所行动,所以不被调用,那么他就不会被创建。
懒汉两式之第一式:
import java.io.Serializable;
/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton implements Serializable {
private static Singleton INST = null;
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
if (INST == null){
INST = new Singleton();
}
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试懒汉式的方法");
}
}
上述饿汉式中可以被破坏,从而创建新的对象,那么懒汉式会不会也会被创建多个多个实例,从而破坏单例模式。
public static Singleton getInstance(){
if (INST == null){ // 1 判断
INST = new Singleton();//2 创建对象
}
return INST;
}
- 在多线程环境下,线程1调用getInstance()方法执行到 2 创建对象还没结束时,线程2同样进入1判断是否为null,由于线程1
创建对象的操作还未完成,线程2同样走上了创建对象的道路,最终导致创建多个对象,单例又被破坏掉。此时,我们可以用加锁来解决
public static synchronized Singleton getInstance(){
if (INST == null){
INST = new Singleton();
}
return INST;
}
- 使用synchronized固然可以解决此问题,但引发了另外一个问题,只有第一次用到才起到关键作用,达到你想要的效果,但是创建了实例之后的操作就显得画蛇添足->加锁影响到性能,所以书山有路勤为径,学海无涯苦作舟,还得接着学呀!
重点来了:懒汉式(双检索)
这种实际就是对懒汉式做了优化,即在获取锁的之前加了判断
import java.io.Serializable;
/** * @ClassName Singleton * @Descption TODO * Version 1.0 **/
public class Singleton implements Serializable {
// 此处的volatile不可忽略
private static volatile Singleton INST = null;
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
if (INST == null){
synchronized(Singleton.class){
if (null==INST){
INST = new Singleton();
}
}
}
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试懒汉式的方法");
}
}
下面对双检索详细讲解
public static Singleton getInstance(){
if (INST == null){ // 1 判断
synchronized(Singleton.class){ //2 获取锁
if (null==INST){ // 3 判断
INST = new Singleton(); // 创建对象
}
}
}
return INST;
}
- 对于 1 判断,较为容易理解,判断是否为null,解决竞争问题,不会有性能上的损失,那么既然之前有了判断,为什么3处又加了判断?
- 同样的多线程环境,如果如果没有3 判断,并且线程1和线程2同事执行到2 竞争锁的这一步,某个线程抢到资源进入4
创建对象时,创建对象完成,释放锁,此时,另一个线程抢到所资源,进入创建对象的过程,结局不言而喻,又是创建了多个对象,单例被破坏,又GG了,所以是两处判断检查,双检索的懒汉式。
代码中一个关键词,不知是否有注意到:volatile
volatile 有两个作用:
- 1、有序性
- 2、可见性
上述代码中volatile 的作用用到了有序性,防止指令重排序。又来接着学吧!
众所周知,创建对象总共分几步?三步!
- 第一步:冰箱门打开
- 第二步:把对象放进去
- 第三步:把冰箱门关上
哈哈哈哈哈,实际差不多,这样更好理解!
- 1、需要分配内存空间
- 2、调用构造方法(成员变量的初始化赋值)
- 3、静态变量的赋值
问题来了:
- 创建对象这三步,除了第一步分配内存空间,其他两个步骤并没有必然的先后顺序,那么CPU在执行的时候,可能会对这两步骤进行先后顺序的调整,这样在多线程环境下会引发一系列的问题。
public static Singleton getInstance(){
if (INST == null){ // 1 判断
synchronized(Singleton.class){ //2 获取锁
if (null==INST){ // 3 判断
INST = new Singleton(); // 创建对象
}
}
}
return INST;
}
如果创建对象的三步骤经过CPU排序之后最终执行的顺序是
1、需要分配内存空间
2、静态变量的赋值
3、调用构造方法(成员变量的初始化赋值)
那么多线程环境下,线程1正在创建对象,刚好执行完2静态变量的赋值,注意,此时静态变量不为null,线程2执行到了1 判断的时候,直接跳过了锁资源这一步,直接返回,构造方法还未执行,拿到的实例有啥用呢?乱七八糟的问题随之而来。
但是当静态变量有了volatile之后,便不会有这种场景,因为volatile会在静态变量赋值语句之后加入内存屏障,阻止了CPU指令排序,让构造方法这步操作越过屏障,在静态变量赋值之后执行,简单的来讲,它的作用就是可以让静态变量在最后一步才执行。
总结
作为技术型文章,没有华丽的语言和生动的文字,有的仅仅是数行代码和对其枯燥的描述,希望本文能对即将开始写作的你,有所帮助。
本文为作者原创文章,未经原创作者同意不得转载