如何实现单例模式及不同实现方法分析-设计模式

这是 一道面试常考题:(经常会在面试中让手写一下)

什么是单例模式

【问什么是单例模式时,不要答非所问,给出单例模式有两种类型之类的回答,要围绕单例模式的定义去展开。】

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

单例模式(Singleton Pattern)是一种常用的设计模式,保证一个类在内存中只有一个实例,并提供一个全局访问点。单例模式通常用于管理共享资源、控制全局状态或提供全局服务。

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

单例模式实现方法:

一、饿汉式单例:在类初始化时就已经自行实例化了
      public class Singleton {
          //私有静态成员变量
          private static Singleton instance = new Singleton();
          //私有构造方法
          private Singleton(){}
          //公有静态访问方法
          public static Singleton getInstance()
          {
              return instance;
          }
      }

注意上面的代码在第2行已经实例化好了一个Singleton对象在内存中,不会有多个Singleton对象实例存在;类在加载时会在堆内存中创建一个Singleton对象当类被卸载时,Singleton对象也随之消亡了。

当然可以改为静态方块来执行实例化语句:

              private static Singleton instance = null;
              static{
                instance = new Singleton();
            }

二、懒汉式单例:在第一次调用实例的时候才实例化

如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了,就不是单例了,所以可以在方法上加锁或类 对象上  加锁,

      public class Singleton {
          //私有静态成员变量
          private static Singleton instance;
          //私有构造方法
          private Singleton(){}
          //公有静态访问方法,在方法上加了一个synchronized关键字确保线程安全
          public static synchronized Singleton getInstance()
          {
              if(instance == null)
                  instance = new Singleton();
              return instance;
          }
      }

         // 或者(在类对象上加锁)   
        public static Singleton getInstance() {
            synchronized(Singleton.class) {   
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
            return singleton;
        }

这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

接下来要做的就是优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁

接下来有下面的DCL

三、双重检测锁模式的懒汉式单例:(线程安全效率高)

        又叫DCL懒汉式 (Double Check Lock)

      public class Singleton {
          //私有静态成员变量,加上了volatile关键字确保可见性
          private volatile static Singleton instance = null;
          //私有构造方法
          private Singleton(){}
          //公有静态访问方法
          public static  Singleton getInstance(){
              if(instance == null){ //线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
                  synchronized (Singleton.class){ //线程A或线程B获得该锁进行初始化;获取锁这里利用到volatile关键字的可见性,再次判断空
                      if(instance == null) //其中一个线程进入该分支,另外一个线程则不会进入该分支,此时instance真的为空,才去创建实例
                          instance = new Singleton();
                  }
              }
              return instance;
          }
      }

 

注意:synchronized 解决并发问题,但是因为lazyMan = new LazyMan();不是原子性操作(可以分割,见代码注释),可能发生指令重排序的问题,通过volatil来解决

  • Java 语言提供了 volatile和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
  • 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;

 指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

  • 使用volatile关键字可以防止指令重排序,​其原理较为复杂,这篇博客不打算展开,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。
  • volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
四、破坏懒汉式单例与饿汉式单例

无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

1.演示利用反射破坏单例模式

public static void main(String[] args) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}

上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象

 2.利用序列化与反序列化破坏单例模式

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

 两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。

五、枚举实现

至此我们已经掌握了懒汉式与饿汉式的常见写法了,在《大话设计模式》中的单例模式章节也止步于此。但是,追求极致的我们,怎么能够止步于此,在《Effective Java》书中,给出了终极解决方法,话不多说,学完下面,真的不虚面试官考你了。

在 JDK1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举

我们先来看看枚举如何实现单例模式的,如下代码:

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("这是枚举类型的单例模式!");
    }
}

需要思考:使用枚举实现单例模式的优势在哪里?

我们从最直观的地方入手,第一眼看到这几行代码,就会感觉到“少”,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。

优势1:代码对比饿汉式与懒汉式来说,更加地简洁

其次,既然是实现单例模式,那这种写法必定满足单例模式的要求,而且使用枚举实现时,没有做任何额外的处理。

优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性

我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。

我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化

public enum Singleton {
    INSTANCE;
    Singleton() { System.out.println("枚举创建对象了"); }
    public static void main(String[] args) { /* test(); */ }
    public void test() {
        Singleton t1 = Singleton.INSTANCE;
        Singleton t2 = Singleton.INSTANCE;
        System.out.print("t1和t2的地址是否相同:" + t1 == t2);
    }
}
// 枚举创建对象了
// t1和t2的地址是否相同:true

除了优势1和优势2,还有最后一个优势让枚举实现单例模式在目前看来已经是“无懈可击”了。

优势3:使用枚举可以防止调用者使用反射序列化与反序列化机制强制生成多个单例对象,破坏单例模式。

防破坏的原理如下:

(1)防反射

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。

(2)防止反序列化创建多个枚举对象

在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

小总结:

(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象

(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象

(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。

总结:

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(3)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/679168.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx location 与 Rewrite

Nginx正则表达式 location 通过前缀或正则匹配用户的URL访问路径做页面跳转、访问控制和代理转发 location 大致可以分为三类&#xff1a; 精准匹配&#xff1a;location / {...} 一般匹配&#xff1a;location / {...} 正则匹配&#xff1a;location ~ / {...} location…

外汇天眼:Bitpanda 扩大与德意志银行的合作

金融科技独角兽Bitpanda正在扩大与德意志银行的合作&#xff0c;为德国用户提供实时支付解决方案&#xff0c;以处理进出交易。 这种基于API的账户解决方案将使Bitpanda能够访问德国的IBAN账户&#xff0c;优化和增强用户体验&#xff0c;同时确保信任、速度和效率。 这只是Bi…

通过仪器分类方式修订看监测仪器发展新趋势

随着科技的进步和监测需求的不断升级&#xff0c;监测仪器的分类方式亟需与时俱进。本文旨在探讨《混凝土坝监测仪器系列型谱》中对现有仪器分类方式的修订&#xff0c;以及监测仪器发展的新趋势相关内容。 一、仪器分类方式的修订 传统的仪器分类方式往往基于功能、原理或应用…

太极图形课——渲染——光线追踪实战第一部分呢

根据概念部分我们逐步通过太极实现光线追踪 总共可以分为5步 第一步&#xff1a;如何发射出一道光&#xff1f; 首先明确何为一道光&#xff0c;光从我们眼睛&#xff08;摄像机&#xff09;射出&#xff0c;那么在三维虚拟世界里&#xff0c;我们可以认为这道光就是一条射线…

【微信小程序】事件绑定和事件对象

文章目录 1.什么是事件绑定2.button组件3.事件绑定4.input组件 1.什么是事件绑定 小程序中绑定事件与在网页开发中绑定事件几乎一致&#xff0c;只不过在小程序不能通过on的方式绑定事件&#xff0c;也没有click等事件&#xff0c;小程序中 绑定事件使用bind方法&#xff0c;c…

6个音效、配乐素材网站,免费可商用

视频剪辑必备的6个音效、配乐素材网站&#xff0c;免费下载&#xff0c;剪辑师们赶紧收藏&#xff01; 1、菜鸟图库 音效素材下载_mp3音效大全 - 菜鸟图库 菜鸟图库音效素材免费下载。站内不仅有大量音频素材&#xff0c;还有很多设计、办公、图片、视频等素材。音频素材全部都…

2024年端午节放假通知

致尊敬的客户以及全体同仁&#xff1a; 2024年端午节将至&#xff0c;根据国务院办公厅通知精神&#xff0c;结合公司的实际情况&#xff0c;现将放假事宜通知如下&#xff1a; 2024年6月8日&#xff08;星期六&#xff09;至6月10日&#xff08;星期一&#xff09;&#xff…

Linux文本处理三剑客之awk命令

官方文档&#xff1a;https://www.gnu.org/software/gawk/manual/gawk.html 什么是awk&#xff1f; Awk是一种文本处理工具&#xff0c;它的名字是由其三位创始人&#xff08;Aho、Weinberger和Kernighan&#xff09;的姓氏首字母组成的。Awk的设计初衷是用于处理结构化文本数…

Spring boot实现基于注解的aop面向切面编程

Spring boot实现基于注解的aop面向切面编程 背景 从最开始使用Spring&#xff0c;AOP和IOC的理念就深入我心。正好&#xff0c;我需要写一个基于注解的AOP&#xff0c;被这个注解修饰的参数和属性&#xff0c;就会被拿到参数并校验参数。 一&#xff0c;引入依赖 当前sprin…

OBD诊断协议

上周领导需要做个OBD相关的功能&#xff0c;我对OBD没有啥概念&#xff0c;于是周末就了解下这到底是个啥东西。了解过后发现很简单&#xff0c;其实就是个UDS协议的简化版&#xff0c;OBD是英文On-Board Diagnostics的缩写&#xff0c;中文翻译为“车载自动诊断系统”&#xf…

Redisson 分布式锁 - RLock、RReadWriteLock、RSemaphore、RCountDownLatch(配置、使用、原理)

目录 前言 Redisson 分布式锁 环境配置 1&#xff09;版本说明 2&#xff09;依赖如下 3&#xff09;配置文件如下 4&#xff09;项目配置 RLock 1&#xff09;使用方式 2&#xff09;加锁解释 3&#xff09;加锁时手动设置时间 4&#xff09;加锁时&#xff0c;到…

JVM运行数据区-Java堆

Java堆 堆区&#xff08;Heap区&#xff09;是JVM运行时数据区占用内存最大的一块区域&#xff0c;每一个JVM进程只存在一个堆区&#xff0c;它在JVM启动时被创建&#xff0c;JVM规范中规定堆区可以是物理上不连续的内存&#xff0c;但必须是逻辑上连续的内存。 1、堆区是线程…

王学岗鸿蒙开发(北向)——————(一)鸿蒙开发环境的搭建与ArkTs介绍

1&#xff0c;鸿蒙系统开始研发的时间是在2012年。 2&#xff0c;目前鸿蒙有两个开发:HarmonyOS和OpenHarmony,前者内聚AOSP(Android的东西)&#xff0c;前者是双框架结构&#xff0c;后者不是双框架结构&#xff0c;没有内置安卓。 3&#xff0c;Harmony地址 4&#xff0c;我们…

训练Pytorch深度学习模型出现StopIteration

训练一个深度学习检测模型&#xff0c;突然出现&#xff1a; 是因为next(batch_iterator)&#xff0c;可能迭代器读出来的数据为空。 # load train data# 原先代码images, targets next(batch_iterator)# 更改为&#xff1a;try:images, targets next(batch_iterator)except…

对接钉钉登陆步骤

背景 之前事情较少的时候&#xff0c;帮公司写过一个系统&#xff0c; 这个系统的话主管有要求要对接钉钉登陆。 话不多说我们直接开干。流程 先进入开发者平台点击开发者后台 没有组织的 我们先在手机上先创建一个组织 创建完成后&#xff0c;就可以看到这个组织了 创建…

Michael.W基于Foundry精读Openzeppelin第56期——VestingWallet.sol

Michael.W基于Foundry精读Openzeppelin第56期——VestingWallet.sol 0. 版本0.1 VestingWallet.sol 1. 目标合约2. 代码精读2.1 constructor()2.2 beneficiary() && start() && duration() && receive() payable2.3 released() && releasable(…

加密经济浪潮:探索Web3对金融体系的颠覆

随着区块链技术的快速发展&#xff0c;加密经济正在成为全球金融领域的一股新的浪潮。而Web3作为下一代互联网的代表&#xff0c;以其去中心化、可编程的特性&#xff0c;正深刻影响着传统金融体系的格局和运作方式。本文将深入探讨加密经济对金融体系的颠覆&#xff0c;探索We…

普通人下班可以做点什么补偿家用

你我&#xff0c;或者说大多数的都是普通人&#xff0c;每个人都在为了生活奔波&#xff0c;没有惊天动地的才华&#xff0c;也没有一夜暴富的运气&#xff0c;但我们依然可以通过自己的双手和智慧&#xff0c;为家庭添上一份温馨。白天的工作往往只能满足基本的生活需求&#…

IIS7整合Tomcat9服务器,并搭建ASP+PHP+JSP完整运行环境

本文以Windows Vista系统为例&#xff0c;详细讲解IIS7整合Tomcat服务器&#xff0c;同时支持ASPPHPJSP三种Web动态网页技术的方法。 Vista系统自带的IIS版本为7.0&#xff0c;能安装的IE浏览器的最高版本为IE9。IE9也是Vue2前端框架支持的最低浏览器版本。 【准备工作】 去微…

第六讲:AD、DA的工作原理及实现、运放电路

DA 数模转换器 (DAC) 数模转换器&#xff08;Digital-to-Analog Converter&#xff0c;简称DAC&#xff09;是一种将数字信号转换为模拟信号的电子装置。DAC在各种电子设备中广泛应用&#xff0c;如音频设备、通信系统、测量设备和控制系统中。以下是DAC的主要概念和应用。…