单例模式有几种写法?

news2024/12/25 1:29:00

作者:猴子007

选自:https://monkeysayhi.github.io

“你知道茴香豆的‘茴’字有几种写法吗?”

纠结单例模式有几种写法有用吗?有点用,面试中经常选择其中一种或几种写法作为话头,考查设计模式和coding style的同时,还很容易扩展到其他问题。

这里讲解几种笔者常用的写法,但切忌生搬硬套,去记“茴香豆的写法”。编程最大的乐趣在于“know everything, control everything”。

JDK版本:oracle java 1.8.0_102

大体可分为4类,下面分别介绍他们的基本形式、变种及特点。

饱汉模式

饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。

基础的饱汉

饱汉,即已经吃饱,不着急再吃,饿的时候再吃。所以他就先不初始化单例,等第一次使用的时候再初始化,即“懒加载”。

// 饱汉
// UnThreadSafe
public class Singleton1 {
  private static Singleton1 singleton = null;
  private Singleton1() {
  }
  public static Singleton1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1();
    }
    return singleton;
  }
}

饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;小坏处是写起来麻烦,大坏处是线程不安全,if语句存在竞态条件。

写起来麻烦不是大问题,可读性好啊。因此,单线程环境下,基础饱汉是笔者最喜欢的写法。但多线程环境下,基础饱汉就彻底不可用了。下面的几种变种都在试图解决基础饱汉线程不安全的问题。

饱汉 - 变种 1

最粗暴的犯法是用synchronized关键字修饰getInstance()方法,这样能达到绝对的线程安全。

// 饱汉
// ThreadSafe
public class Singleton1_1 {
  private static Singleton1_1 singleton = null;
  private Singleton1_1() {
  }
  public synchronized static Singleton1_1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1_1();
    }
    return singleton;
  }
}

变种1的好处是写起来简单,且绝对线程安全;坏处是并发性能极差,事实上完全退化到了串行。单例只需要初始化一次,但就算初始化以后,synchronized的锁也无法避开,从而getInstance()完全变成了串行操作。性能不敏感的场景建议使用。

饱汉 - 变种 2

变种2是“臭名昭著”的DCL 1.0。

针对变种1中单例初始化后锁仍然无法避开的问题,变种2在变种1的外层又套了一层check,加上synchronized内层的check,即所谓“双重检查锁”(Double Check Lock,简称DCL)。

// 饱汉
// UnThreadSafe
public class Singleton1_2 {
  private static Singleton1_2 singleton = null;

  public int f1 = 1;   // 触发部分初始化问题
  public int f2 = 2;
  private Singleton1_2() {
  }
  public static Singleton1_2 getInstance() {
    // may get half object
    if (singleton == null) {
      synchronized (Singleton1_2.class) {
        if (singleton == null) {
          singleton = new Singleton1_2();
        }
      }
    }
    return singleton;
  }
}

变种2的核心是DCL,看起来变种2似乎已经达到了理想的效果:懒加载+线程安全。可惜的是,正如注释中所说,DCL仍然是线程不安全的,由于指令重排序,你可能会得到“半个对象”,即”部分初始化“问题。详细在看完变种3后,可参考下面这篇文章,这里不再赘述。

https://monkeysayhi.github.io/2016/11/29/volatile关键字的作用、原理/

饱汉 - 变种 3

变种3专门针对变种2,可谓DCL 2.0。

针对变种3的“半个对象”问题,变种3在instance上增加了volatile关键字,原理见上述参考。

// 饱汉
// ThreadSafe
public class Singleton1_3 {
  private static volatile Singleton1_3 singleton = null;

  public int f1 = 1;   // 触发部分初始化问题
  public int f2 = 2;
  private Singleton1_3() {
  }
  public static Singleton1_3 getInstance() {
    if (singleton == null) {
      synchronized (Singleton1_3.class) {
        // must be a complete instance
        if (singleton == null) {
          singleton = new Singleton1_3();
        }
      }
    }
    return singleton;
  }
}

多线程环境下,变种3更适用于性能敏感的场景。但后面我们将了解到,就算是线程安全的,还有一些办法能破坏单例。

当然,还有很多方式,能通过与volatile类似的方式防止部分初始化。读者可自行阅读内存屏障相关内容,但面试时不建议主动装逼。

饿汉模式

与饱汉相对,饿汉很饿,只想着尽早吃到。所以他就在最早的时机,即类加载时初始化单例,以后访问时直接返回即可。

// 饿汉
// ThreadSafe
public class Singleton2 {
  private static final Singleton2 singleton = new Singleton2();
  private Singleton2() {
  }
  public static Singleton2 getInstance() {
    return singleton;
  }
}

饿汉的好处是天生的线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能造成资源浪费(如果类加载后就一直不使用单例的话)。

值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于饱汉需要加锁,饿汉的性能反而更优。

Holder模式

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。

// Holder模式
// ThreadSafe
public class Singleton3 {
  private static class SingletonHolder {
    private static final Singleton3 singleton = new Singleton3();
    private SingletonHolder() {
    }
  }
  private Singleton3() {
  }

  /**
  * 勘误:多写了个synchronized。。
  public synchronized static Singleton3 getInstance() {
    return SingletonHolder.singleton;
  }
  */
  public static Singleton3 getInstance() {
    return SingletonHolder.singleton;
  }
}

相对于饿汉模式,Holder模式仅增加了一个静态内部类的成本,与饱汉的变种3效果相当(略优),都是比较受欢迎的实现方式。同样建议考虑。

枚举模式

用枚举实现单例模式,相当好用,但可读性是不存在的。

基础的枚举

将枚举的静态成员变量作为单例的实例:

// 枚举
// ThreadSafe
public enum Singleton4 {
  SINGLETON;
}

代码量比饿汉模式更少。但用户只能直接访问实例Singleton4.SINGLETON——事实上,这样的访问方式作为单例使用也是恰当的,只是牺牲了静态工厂方法的优点,如无法实现懒加载。

丑陋但好用的语法糖

Java的枚举是一个“丑陋但好用的语法糖”。

枚举型单例模式的本质

通过反编译打开语法糖,就看到了枚举类型的本质,简化如下:

// 枚举
// ThreadSafe
public class Singleton4 extends Enum<Singleton4> {
  ...
  public static final Singleton4 SINGLETON = new Singleton4();
  ...
}

本质上和饿汉模式相同,区别仅在于公有的静态成员变量。

用枚举实现一些trick

这一部分与单例没什么关系,可以跳过。如果选择阅读也请认清这样的事实:虽然枚举相当灵活,但如何恰当的使用枚举有一定难度。一个足够简单的典型例子是TimeUnit类,建议有时间耐心阅读。

上面已经看到,枚举型单例的本质仍然是一个普通的类。实际上,我们可以在枚举型型单例上增加任何普通类可以完成的功能。要点在于枚举实例的初始化,可以理解为实例化了一个匿名内部类。为了更明显,我们在Singleton4_1中定义一个普通的私有成员变量,一个普通的公有成员方法,和一个公有的抽象成员方法,如下:

// 枚举
// ThreadSafe
public enum Singleton4_1 {
  SINGLETON("enum is the easiest singleton pattern, but not the most readable") {
    public void testAbsMethod() {
      print();
      System.out.println("enum is ugly, but so flexible to make lots of trick");
    }
  };
  private String comment = null;
  Singleton4_1(String comment) {
    this.comment = comment;
  }
  public void print() {
    System.out.println("comment=" + comment);
  }
  abstract public void testAbsMethod();
  public static Singleton4_1 getInstance() {
    return SINGLETON;
  }
}

这样,枚举类Singleton4_1中的每一个枚举实例不仅继承了父类Singleton4_1的成员方法print(),还必须实现父类Singleton4_1的抽象成员方法testAbsMethod()。

总结

上面的分析都忽略了反射和序列化的问题。通过反射或序列化,我们仍然能够访问到私有构造器,创建新的实例破坏单例模式。此时,只有枚举模式能天然防范这一问题。反射和序列化笔者还不太了解,但基本原理并不难,可以在其他模式上手动实现。

下面继续忽略反射和序列化的问题,做个总结回味一下:

alt

单例模式是面试中的常考点,写起来非常简单。

顶尖架构师栈

关注回复关键字

【C01】超10G后端学习面试资源

【IDEA】最新IDEA激活工具和码及教程

【JetBrains软件名】 最新软件激活工具和码及教程

工具&码&教程

转载于:https://mp.weixin.qq.com/s/a3f0VyFdwTi2hkr0JcnqFQ

本文由 mdnice 多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot2.7.15+thymeleaf

如下使用了thymeleaf的基础应用&#xff1a;th:text, th:each, th:if, th:unless, th:value等标签的使用 页面效果&#xff1a;未登录状态 登录状态&#xff1a; 如下的所有html放在templates 下&#xff0c; 配置文件不需要做任何配置 只需要在pom.xml中增加 <dependen…

记一次 .NET 某仪器测量系统 CPU爆高分析

一&#xff1a;背景 1. 讲故事 最近也挺奇怪&#xff0c;看到了两起 CPU 爆高的案例&#xff0c;且诱因也是一致的&#xff0c;觉得有一些代表性&#xff0c;合并分享出来帮助大家来避坑吧&#xff0c;闲话不多说&#xff0c;直接上 windbg 分析。 二&#xff1a;WinDbg 分析…

【ODPS新品发布第2期】实时数仓Hologres:推出计算组实例/支持JSON数据/向量计算+大模型等新能力

阿里云ODPS系列产品以MaxCompute、DataWorks、Hologres为核心&#xff0c;致力于解决用户多元化数据的计算需求问题&#xff0c;实现存储、调度、元数据管理上的一体化架构融合&#xff0c;支撑交通、金融、科研、等多场景数据的高效处理&#xff0c;是目前国内最早自研、应用最…

七、定时器类的编写与解析 —— TinyWebServer

定时器类的编写与解析 —— TinyWebServer 一、前言 定时器非常好写。就是链表加定时函数。搞懂他的作用就成。 定时器的作用是什么&#xff1f;什么是回调函数&#xff1f;用到的函数是什么&#xff1f; 二、问题回答 Ⅰ、定时器的作用是什么&#xff1f; 处理非活跃的连…

Mybatis自动映射Java对象 与 MySQL8后的JSON数据

文章目录 Mybatis自动映射Java对象 与 MySQL8后的JSON数据1.转化成为正常Json类型1.1 JsonTypeHander1.2 ListJsonTypeHandler 负责List<T> 类型1.3 实体类1.4 mapper1.5 测试类 2. 存储为携带类型的Json Mybatis自动映射Java对象 与 MySQL8后的JSON数据 1.转化成为正常…

OPTEE Ftrace函数跟踪

安全之安全(security)博客目录导读 OPTEE调试技术汇总 目录 一、序言 二、Ftrace配置 三、Ftrace使用 四、Ftrace典型输出 一、序言 本节描述如何使用ftrace为TA生成函数调用图。该名称来自具有类似目的的Linux框架&#xff0c;但是OP-TEE ftrace非常具体…

前端代码统计工具之cloc介绍

目录 一、安装 二 使用命令cloc path&#xff1a; 使用cloc工具可以很好的统计出前端的代码量&#xff0c;经过亲身实践真的很棒。 一、安装 安装命令 pnpm add cloc -g 二 使用命令cloc path&#xff1a; cloc ./ 如果报错&#xff1a; perl 不是内部或外部命令的问…

风靡全国的真人猫抓老鼠是什么?

某音上这个词条2.6亿&#xff0c;小某书上1.2亿。据说已经风靡全国50多个城市了。各大新闻网站和自媒体人争相报道&#xff0c;热度直接拉满&#xff01; 现在的年轻人真会玩&#xff01; 仔细了解过后发现&#xff0c;它火是有内在原因的&#xff0c;现在都市工作后没有可以…

近年来国内室内定位领域硕士论文选题的现状与趋势

目录 一、前言 二、选题的目的和意义 三、选题现状分析 四、选题趋势分析 一、前言 本博文采用了图表统计法分析了近5年来100余篇高被引室内定位领域硕士论文选题的现状&#xff0c;并从选题现状中得出了该领域选题的大致趋势。本文还通过分析该领域硕士毕业论文选题的现…

只需100GB内存,让Falcon 180B在你的电脑上起飞

一、前言 自2023年5月&#xff0c;阿布扎比技术创新研究所&#xff08;TII&#xff09;发布了两个预训练的LLM&#xff1a;Falcon 7B和Falcon-40B&#xff0c;这两个模型的表现十分优异&#xff0c;在OpenLLM排行榜上高居榜首。然而&#xff0c;在短短不到几个月的时间&#x…

Sftp服务安全评估

1 认识SFTP FTP&#xff08;SSH文件传输协议&#xff09;和FTP&#xff08;文件传输协议&#xff09;是两种用于文件传输的协议&#xff0c;它们在工作原理、安全性和配置方面有很大的差异。 1&#xff09;工作原理&#xff1a; FTP&#xff1a;FTP使用两个独立的连接&#…

【MySql】1- 基础篇(上)

文章目录 1.1 前言1.2 基础架构1.2.1 MySql基本架构示意图1.2.2 SQL语句执行顺序 1.3 日志系统&#xff1a;一条SQL更新语句如何执行1.3.1 两阶段提交 1.4 事务隔离1.4.1 隔离性与隔离级别1.4.2 事务隔离的实现-展开说明“可重复读”1.4.3 事务的启动方式 1.5 深入浅出索引1.5.…

什么是实时操作系统(UCOS简介)

uC/OS-III官网&#xff1a;Home Page - Weston Embedded Solutions 一、裸机与RTOS介绍 下面我将从不同方面阐述裸机与试试操作系统的区别&#xff0c;从而进一步介绍裸机和实时操作系统 定义&#xff1a; 裸机&#xff1a;裸机指的是没有任何操作系统或软件层的硬件系统。在…

Linux C 网络基础

为什么需要网络通信&#xff1f; 进程间通信解决的是本机内通信 网络通信解决的是任意不同机器的通信 实现网络通信需要哪些支持 1.通信设备&#xff1a;网卡&#xff08;PC机自带&#xff09;&#xff1b; 路由器和交换机&#xff1b; 光纤…

Android:ListView在Fragment中的使用

一、前言&#xff1a; 因为工作一直在用mvvm框架&#xff0c;因此这篇文章是基于mvvm框架写的。在Fragment复制之前一定要谨记项目可以跑起来。确保能跑起来之后直接复制就行。 二、代码展示&#xff1a; 页面布局 ?xml version"1.0" encoding"utf-8"…

Cortex-M3/M4基础

一、Cortex-M3/M4 通用寄存器 1、我们首先来了解一下M3/M4的寄存器&#xff0c;M4比M3多了一个浮点单元FPU。其他的部分基本和M3是一样的。 2、Cortex-M3/M4系列处理器拥有通用寄存器R0-R15以及一些特殊功能的寄存器。 3、R0‐ R12 是最“通用目的”的。 4、但是绝大多数的…

UEFI 安装 Debian12 Linux 物理机虚拟机VMware通用

文章目录 前言⭐前置虚拟机物理机 安装流程选择安装方式语言及键盘选择网络选择创建用户系统磁盘分区新旧磁盘分区方式BOOT分区SWAP分区根分区 安装过程中其他选项选择软件包安装流程末 前言⭐ 物理机和虚拟机安装仅有设置UFFI引导的差别、这里前置为设置UEFI引导。安装步骤大…

干货 | 中国石化化工高端新材料价格体系模型构建

以下内容整理自2023年夏季学期大数据能力提升项目《大数据实践课》同学们所做的期末答辩汇报。 随着石化行业市场日趋饱和&#xff0c;市场竞争日益激烈&#xff0c;企业利润空间不断被压缩&#xff0c;大多数石化企业急需转型开拓新市场&#xff0c;化工原料价格的波动对于石化…

数据结构 - 线性表(顺序表)

线性表是什么 线性表是包含若干数据元素的一个线性序列&#xff0c;记为&#xff1a; L (a0&#xff0c;…ai-1&#xff0c;ai,ai1,…an-1) L为表名&#xff0c;ai&#xff08;0≤ i ≤n-1&#xff09;为数据元素&#xff1b;n为表长&#xff0c;n>0时&#xff0c;线性表…

Vue的详细教程--用Vue-cli搭建SPA项目

Vue的详细教程--用Vue-cli搭建SPA项目 1.Vue-cli是什么2.什么是SPA项目1.vue init webpack spa2.一问一答模式2&#xff1a;运行完上面的命令后&#xff0c;我们需要将当前路径改变到SPA这个文件夹内&#xff0c;然后安装需要的模块此步骤可理解成&#xff1a;maven的web项目创…