百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

我用 DCL 写出了单例模式,结果阿里面试官不满意

bigegpt 2025-03-02 16:15 9 浏览

本文已收录到我的 github 地址:
https://github.com/allentofight/easy-cs
,欢迎大家关注并给个 star,这对坤哥非常重要,感谢支持!之后码海的每篇文章都会收录至此地址以方便大家查阅!

前言

单例模式可以说是设计模式中最简单和最基础的一种设计模式了,哪怕是一个初级开发,在被问到使用过哪些设计模式的时候,估计多数会说单例模式。但是你认为这么基本的”单例模式“真的就那么简单吗?或许你会反问:「一个简单的单例模式该是咋样的?」哈哈,话不多说,让我们一起拭目以待,坚持看完,相信你一定会有收获!

饿汉式

饿汉式是最常见的也是最不需要考虑太多的单例模式,因为他不存在线程安全问题,饿汉式也就是在类被加载的时候就创建实例对象。饿汉式的写法如下:

public?class?SingletonHungry?{
????private?static?SingletonHungry?instance?=?new?SingletonHungry();

????private?SingletonHungry()?{
????}

????private?static?SingletonHungry?getInstance()?{
????????return?instance;
????}
}
  • 测试代码如下:
class?A?{
????public?static?void?main(String[]?args)?{
????????IntStream.rangeClosed(1,?5)
????????????????.forEach(i?->?{
????????????????????new?Thread(
????????????????????????????()?->?{
????????????????????????????????SingletonHungry?instance?=?SingletonHungry.getInstance();
????????????????????????????????System.out.println("instance?=?"?+?instance);
????????????????????????????}
????????????????????).start();
????????????????});
????}
}

结果

优点:线程安全,不需要关心并发问题,写法也是最简单的。

缺点:在类被加载的时候对象就会被创建,也就是说不管你是不是用到该对象,此对象都会被创建,浪费内存空间

懒汉式

以下是最基本的饿汉式的写法,在单线程情况下,这种方式是非常完美的,但是我们实际程序执行基本都不可能是单线程的,所以这种写法必定会存在线程安全问题

public?class?SingletonLazy?{
????private?SingletonLazy()?{
????}

????private?static?SingletonLazy?instance?=?null;

????public?static?SingletonLazy?getInstance()?{
????????if?(null?==?instance)?{
????????????return?new?SingletonLazy();
????????}
????????return?instance;

????}
}

演示多线程执行

class?B?{
????public?static?void?main(String[]?args)?{
????????IntStream.rangeClosed(1,?5)
????????????????.forEach(i?->?{
????????????????????new?Thread(
????????????????????????????()?->?{
????????????????????????????????SingletonLazy?instance?=?SingletonLazy.getInstance();
????????????????????????????????System.out.println("instance?=?"?+?instance);
????????????????????????????}
????????????????????).start();
????????????????});
????}
}

结果

结果很显然,获取的实例对象不是单例的。也就是说这种写法不是线程安全的,也就不能在多线程情况下使用

DCL(双重检查锁式)

DCL 即 Double Check Lock 就是在创建实例的时候进行双重检查,首先检查实例对象是否为空,如果不为空将当前类上锁,然后再判断一次该实例是否为空,如果仍然为空就创建该是实例;代码如下:

public?class?SingleTonDcl?{
????private?SingleTonDcl()?{
????}

????private?static?SingleTonDcl?instance?=?null;

????public?static?SingleTonDcl?getInstance()?{
????????if?(null?==?instance)?{
????????????synchronized?(SingleTonDcl.class)?{
????????????????if?(null?==?instance)?{
????????????????????instance?=?new?SingleTonDcl();
????????????????}
????????????}
????????}
????????return?instance;
????}
}

测试代码如下:

class?C?{
????public?static?void?main(String[]?args)?{
????????IntStream.rangeClosed(1,?5)
????????????????.forEach(i?->?{
????????????????????new?Thread(
????????????????????????????()?->?{
????????????????????????????????SingleTonDcl?instance?=?SingleTonDcl.getInstance();
????????????????????????????????System.out.println("instance?=?"?+?instance);
????????????????????????????}
????????????????????).start();
????????????????});
????}
}

结果

相信大多数初学者在接触到这种写法的时候已经感觉是「高大上」了,首先是判断实例对象是否为空,如果为空那么就将该对象的 Class 作为锁,这样保证同一时刻只能有一个线程进行访问,然后再次判断实例对象是否为空,最后才会真正的去初始化创建该实例对象。一切看起来似乎已经没有破绽,但是当你学过JVM后你可能就会一眼看出猫腻了。没错,问题就在 instance = new SingleTonDcl(); 因为这不是一个原子的操作,这句话的执行是在 JVM 层面分以下三步:

1.给 SingleTonDcl 分配内存空间 2.初始化 SingleTonDcl 实例 3.将 instance 对象指向分配的内存空间( instance 为 null 了)

正常情况下上面三步是顺序执行的,但是实际上JVM可能会「自作多情」得将我们的代码进行优化,可能执行的顺序是1、3、2,如下代码所示

public?static?SingleTonDcl?getInstance()?{
????if?(null?==?instance)?{
????????synchronized?(SingleTonDcl.class)?{
????????????if?(null?==?instance)?{
????????????????1.?给?SingleTonDcl?分配内存空间
????????????????3.将?instance?对象指向分配的内存空间(?instance?不为?null?了)
????????????????2.?初始化?SingleTonDcl?实例
????????????}
????????}
????}
????return?instance;
}

假设现在有两个线程 t1, t2

  1. 如果 t1 执行到以上步骤 3 被挂起
  2. 然后 t2 进入了 getInstance 方法,由于 t1 执行了步骤 3,此时的 instance 已经不为空了,所以 if (null == instance) 这个条件不为空,直接返回 instance, 但由于 t1 还未执行步骤 2,导致此时的 instance 实际上是个半成品,会导致不可预知的风险!

该怎么解决呢,既然问题出在指令有可能重排序上,不让它重排序不就行了,volatile 不就是干这事的吗,我们可以在 instance 变量前面加上一个 volatile 修饰符

画外音:volatile 的作用
1.保证的对象内存可见性
2.防止指令重排序

优化后的代码如下

public?class?SingleTonDcl?{
????private?SingleTonDcl()?{
????}

????//在对象前面添加?volatile?关键字即可
????volatile?private?static?SingleTonDcl?instance?=?null;

????public?static?SingleTonDcl?getInstance()?{
????????if?(null?==?instance)?{
????????????synchronized?(SingleTonDcl.class)?{
????????????????if?(null?==?instance)?{
????????????????????instance?=?new?SingleTonDcl();
????????????????}
????????????}
????????}
????????return?instance;
????}
}

到这里似乎问题已经解决了,双重锁机制 + volatile 实际上确实基本上解决了线程安全问题,保证了“真正”的单例。但真的是这样的吗?继续往下看

静态内部类

先看代码

public?class?SingleTonStaticInnerClass?{
????private?SingleTonStaticInnerClass()?{

????}

????private?static?class?HandlerInstance?{
????????private?static?SingleTonStaticInnerClass?instance?=?new?SingleTonStaticInnerClass();
????}

????public?static?SingleTonStaticInnerClass?getInstance()?{
????????return?HandlerInstance.instance;
????}
}
  • 测试代码如下:
class?D?{
????public?static?void?main(String[]?args)?{
????????IntStream.rangeClosed(1,?5)
????????????????.forEach(i->{
????????????????????new?Thread(()->{
????????????????????????SingleTonStaticInnerClass?instance?=?SingleTonStaticInnerClass.getInstance();
????????????????????????System.out.println("instance?=?"?+?instance);
????????????????????}).start();
????????????????});
????}
}

静态内部类的特点:

这种写法使用 JVM 类加载机制保证了线程安全问题;由于 SingleTonStaticInnerClass 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本;

但是,它依旧不是完美的。

不安全的单例

上面实现单例都不是完美的,主要有两个原因

1. 反射攻击

首先要提到 java 中让人又爱又恨的反射机制, 闲言少叙,我们直接边上代码边说明,这里就以 DCL 举例(为什么选择 DCL 因为很多人觉得 DCL 写法是最高大上的....这里就开始去”打他们的脸“)

将上面的 DCl 的测试代码修改如下:

class?C?{
????public?static?void?main(String[]?args)?throws?NoSuchMethodException,?IllegalAccessException,?InvocationTargetException,?InstantiationException?{
????????Class?singleTonDclClass?=?SingleTonDcl.class;
????????//获取类的构造器
????????Constructor?constructor?=?singleTonDclClass.getDeclaredConstructor();
????????//把构造器私有权限放开
????????constructor.setAccessible(true);
????????//反射创建实例???注意反射创建要放在前面,才会攻击成功,因为如果反射攻击在后面,先使用正常的方式创建实例的话,在构造器中判断是可以防止反射攻击、抛出异常的,
????????//因为先使用正常的方式已经创建了实例,会进入if
????????SingleTonDcl?instance?=?constructor.newInstance();
????????//正常的获取实例方式???正常的方式放在反射创建实例后面,这样当反射创建成功后,单例对象中的引用其实还是空的,反射攻击才能成功
????????SingleTonDcl?instance1?=?SingleTonDcl.getInstance();
????????System.out.println("instance1?=?"?+?instance1);
????????System.out.println("instance?=?"?+?instance);
????}
}

居然是两个对象!内心是不是异常平静?果然和你想的不一样?其他的方式基本类似,都可以通过反射破坏单例。

2. 序列化攻击

我们以「饿汉式单例」为例来演示一下序列化和反序列化攻击代码,首先给饿汉式单例对应的类添加实现 Serializable 接口的代码,

public?class?SingletonHungry?implements?Serializable?{
????private?static?SingletonHungry?instance?=?new?SingletonHungry();

????private?SingletonHungry()?{
????}

????private?static?SingletonHungry?getInstance()?{
????????return?instance;
????}
}

然后看看如何使用序列化和反序列化进行攻击

SingletonHungry?instance?=?SingletonHungry.getInstance();
ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("singleton_file")));
//?序列化【写】操作
oos.writeObject(instance);
File?file?=?new?File("singleton_file");
ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(file))
//?反序列化【读】操作
SingletonHungry?newInstance?=?(SingletonHungry)?ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance?==?newInstance);

来看下结果

果然出现了两个不同的对象!这种反序列化攻击其实解决方式也简单,重写反序列化时要调用的 readObject 方法即可

private?Object?readResolve(){
????return?instance;
}

这样在反序列化时候永远只读取 instance 这一个实例,保证了单例的实现。

真正安全的单例: 枚举方式

public?enum?SingleTonEnum?{
????/**
?????*?实例对象
?????*/
????INSTANCE;
????public?void?doSomething()?{
????????System.out.println("doSomething");
????}
}

调用方法

public?class?Main?{
????public?static?void?main(String[]?args)?{
????????SingleTonEnum.INSTANCE.doSomething();
????}
}

枚举模式实现的单例才是真正的单例模式,是完美的实现方式

有人可能会提出疑问:枚举是不是也能通过反射来破坏其单例实现呢?

试试呗,修改枚举的测试类

class?E{
????public?static?void?main(String[]?args)?throws?NoSuchMethodException,?IllegalAccessException,?InvocationTargetException,?InstantiationException?{
????????Class?singleTonEnumClass?=?SingleTonEnum.class;
????????Constructor?declaredConstructor?=?singleTonEnumClass.getDeclaredConstructor();
????????declaredConstructor.setAccessible(true);
????????SingleTonEnum?singleTonEnum?=?declaredConstructor.newInstance();
????????SingleTonEnum?instance?=?SingleTonEnum.INSTANCE;
????????System.out.println("instance?=?"?+?instance);
????????System.out.println("singleTonEnum?=?"?+?singleTonEnum);
????}
}

结果

没有无参构造?我们使用 javap 工具来查下字节码看看有啥玄机

好家伙,发现一个有参构造器 String Int ,那就试试呗

//获取构造器的时候修改成这样子
Constructor?declaredConstructor?=?singleTonEnumClass.getDeclaredConstructor(String.class,int.class);

结果

好家伙,抛出了异常,异常信息写着: 「Cannot reflectively create enum objects」

源码之下无秘密,我们来看看 newInstance() 到底做了什么?为啥用反射创建枚举会抛出这么个异常?

真相大白!如果是枚举,不允许通过反射来创建,这才是使用 enum 创建单例才可以说是真正安全的原因!

结束语

以上就是一些关于单例模式的知识点汇总,你还真不要小看这个小小的单例,面试的时候多数候选人写不对这么一个简单的单例,写对的多数也仅止于 DCL,但再问是否有啥不安全,如何用 enum 写出安全的单例时,几乎没有人能答出来!有人说能写出 DCL 就行了,何必这么钻牛角尖?但我想说的是正是这种钻牛角尖的精神能让你逐步积累技术深度,成为专家,对技术有一探究竟的执著,何愁成不了专家?

最后欢迎大家关注我的公众号:码海,一起交流,共同进步!

相关推荐

5分钟调色大片的方法(5分钟调色大片的方法有哪些)

哈喽大家好。在大家印象中一定觉得ps非常难学非常难。大家不要着急,小编的教学都是针对ps零基础的同学的,而且非常实用哦。只要大家跟着图文练习一两遍,保证大家立马学会~!好了,废话少说,下面开始我们今天...

闪白特效原来是这么用的(闪白特效怎么使用)

作者|高艳侠订阅|010-86092062闪白特效是影视作品中应用比较多的效果之一,那么具体该在哪些场景使用闪白特效?具体该如何操作?下面就以AdobePremiere(以下简称PR)为例,...

ppt常用小图标去哪里找?3个矢量素材网站推荐!

ppt是一个注重可视化表达的演示载体,除了高清图片,ppt中另一类常用的素材是各种小图标,也叫矢量图标,巧妙运用小图标能提升整体美观度和表现力,那么ppt常用小图标去哪里找呢?为方便各位快速找到合适的...

有什么好用的截图录屏工具?试试这9款

经常有朋友反馈苦于缺乏截屏和录屏的趁手工具,本期我们分享几个相当好用的截屏和录屏工具,希望能帮到大家。ScreenToGifScreenToGif是一款免费且开源的录屏工具。此款工具最大的特点是可以...

配色苦手福音!专业快速色环配色PS插件

今天橘子老师给的大家介绍的是一款快速配色的插件,非常强大配色苦手福音来啦!(获取方式见文末)【插件介绍】配色在后期设计中占有主导地位,好的配色能让作品更加抢眼Coolorus这款专业的配色插件,能够...

如何用PS抠主体?(ps怎么抠主体)

1.主体法抠图-抠花苞和花梗导入一张荷花苞的照片,点击上图中顶部“选择”菜单栏,下拉单击“主体”。可以看到,只有花苞被选中,但是花梗并没有被选中。接下来单击上图中左侧工具栏的“快速选择工具”,上图中顶...

2799元的4K电视,有保障吗?(买4k电视机哪个品牌好)

在上一期《电脑报》的3·15专题报道中,我们揭露了一款不靠谱的42英寸4K智能电视——TCLD42A561U。这款售价2699元的4K智能电视不仅4K画质方面存在严重问题,而且各种功能和应用体验也不理...

苹果电脑的Touch Bar推出一段时间了 这款工具可以帮你开发适用于它的APP

距离苹果推出带有TouchBar的MacBookPro已经有一段时间了,除了那些像Adobe、Google和Microsoft大公司在开发适用于TouchBar的应用之外,其实还有很多独立的开...

如魔法般吸取颜色的桌灯(如魔法般吸取颜色的桌灯叫什么)

色彩为生活带来的感官刺激,逐渐被视为理所当然。一盏桌灯运用它的神奇力量,将隐藏于物件中的颜色逐一释放,成为装点环境的空间魔法师。ColorUp是一款可以改变颜色的吸色台灯,沿用传统灯泡的造型,融入了拾...

一篇文章带你用jquery mobile设计颜色拾取器

【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...

ps拾色器快捷键是什么?(ps2019拾色器快捷键)

ps拾色器快捷键是什么?文章末尾有获取方式,按照以下步骤就能自动获得!学会制作PS特效需要一定程度的耐心和毅力。初学者可以从基本的工具和技术开始学习,逐渐提高他们的技能水平。同时,观看更多优秀的特效作...

免费开源的 Windows 截图录屏工具,支持 OCR 识别和滚动截图等

功能很强大、安装很小巧的免费截图、录屏工具,提供很多使用的工具来帮我么能解决问题,推荐给大家。关于ShareXShareX是一款免费的windows工具,起初是一个小巧的截图工具,经过多年的迭...

入门到精通系列PS教程:第13篇 · 拾色器、颜色问题说明及补充

入门到精通系列PS教程:第13篇·拾色器、颜色问题说明及补充作者|侯潇问题说明我的第12篇教程里,有个小问题没有说清楚。要说是错误,又不算是错误,只是没有说准确。写完那篇教程后,因为已经到了深...

PS冷知识:用吸管工具吸取屏幕上的任意颜色

今天,我们给大家介绍PS中的一个冷知识:用吸管工具可以吸取屏幕上的任意颜色。其实,操作起来是非常简单的。大多数情况下,我们认为,PS的吸管工具只能吸取PS软件作图区域范围内的颜色,最多加上画布四周的...

Windows 11 将提供内置颜色选择器工具

Windows11内置了颜色选择器,可以扫描并识别屏幕上的颜色并生成颜色代码。此外,微软还利用人工智能技术,让屏幕上的文本扫描和选择变得更加便捷。这两项功能均已在SnippingToolv1...