我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何使用单例模式

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何使用单例模式

这篇文章主要讲解了“如何使用单例模式”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何使用单例模式”吧!

饿汉式

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

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. 鸿蒙官方战略合作共建——HarmonyOS技术社区

  2. 如果 t1 执行到以上步骤 3 被挂起

  3. 然后 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<SingleTonDcl> singleTonDclClass = SingleTonDcl.class;         //获取类的构造器         Constructor<SingleTonDcl> 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<SingleTonEnum> singleTonEnumClass = SingleTonEnum.class;         Constructor<SingleTonEnum> 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<SingleTonEnum> declaredConstructor = singleTonEnumClass.getDeclaredConstructor(String.class,int.class);

结果

如何使用单例模式

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

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

如何使用单例模式

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

感谢各位的阅读,以上就是“如何使用单例模式”的内容了,经过本文的学习后,相信大家对如何使用单例模式这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何使用单例模式

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

PHP中如何使用单例模式?

php 中的单例模式确保一个类只有一个实例,通过以下步骤实现:创建私有静态属性存储实例。创建私有构造函数防止直接实例化。创建公共静态方法用于获取实例;如果不存在则创建并存储为私有属性。PHP中的单例模式简介单例模式是设计模式的一种,用于
PHP中如何使用单例模式?
2024-05-21

如何使用Javascript实现单例模式

这篇文章给大家分享的是有关如何使用Javascript实现单例模式的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。JavaScript是什么JavaScript是一种直译式的脚本语言,其解释器被称为JavaScri
2023-06-14

如何使用Python继承实现单例模式

这篇文章给大家分享的是有关如何使用Python继承实现单例模式的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。继承实现:class ParentClass: def __new__(cls, *args, *
2023-06-17

如何使用Python元类实现单例模式

这篇文章主要介绍了如何使用Python元类实现单例模式,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。元类实现:class MetaClass(type): def __
2023-06-17

如何使用Python装饰器实现单例模式

这篇文章主要为大家展示了“如何使用Python装饰器实现单例模式”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用Python装饰器实现单例模式”这篇文章吧。装饰器实现:def warppe
2023-06-17

如何使用Python中的@classmethod实现单例模式

这篇文章主要介绍了如何使用Python中的@classmethod实现单例模式,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。@classmethod实现单例模式:class
2023-06-17

如何使用枚举来实现java单例模式

这篇文章主要介绍“如何使用枚举来实现java单例模式”,在日常操作中,相信很多人在如何使用枚举来实现java单例模式问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何使用枚举来实现java单例模式”的疑惑有所
2023-06-20

php单例模式怎么使用

使用PHP实现单例模式的一种常见方式是通过定义一个私有的静态属性来保存类的实例,并使用一个公共的静态方法来获取该实例。以下是一个简单的示例代码:class Singleton {// 私有静态属性,用于保存类的实例private sta
2023-10-21

如何在Unity3D中使用单例模式和静态类

本篇文章为大家展示了如何在Unity3D中使用单例模式和静态类,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。1、静态类不能继承和被继承!(严格点说是只能继承System.Object)也就是说你的静
2023-06-14

java单例模式如何实现

本篇内容主要讲解“java单例模式如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“java单例模式如何实现”吧!0x01 宫女请安在朕的后宫中,皇后当之无愧的是天下第一(朕只能当第二),为
2023-06-19

JavaScript如何实现单例模式

小编给大家分享一下JavaScript如何实现单例模式,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!单例模式通过 ES6 的 Proxy 拦截构造函数的执行方法来
2023-06-27

Android如何实现单例模式

这篇文章主要介绍了Android如何实现单例模式,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一.饿汉式public class SingletionStarving {
2023-06-15

PHP单例模式如何实现

这篇文章主要介绍“PHP单例模式如何实现”,在日常操作中,相信很多人在PHP单例模式如何实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”PHP单例模式如何实现”的疑惑有所帮助!接下来,请跟着小编一起来学习吧
2023-07-04

java设计模式中如何实现单例模式

这篇文章将为大家详细讲解有关java设计模式中如何实现单例模式,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。下面是一个简单的小实例://简单懒汉式 public class Singleton {
2023-05-30

编程热搜

目录