Java 设计模式实战系列—单例模式

news2024/12/23 19:27:44

本文首发公众号:小码A梦

单例模式是设计模式中最简单一个设计模式,该模式属于创建型模式,它提供了一种创建实例的最佳方式。

单例模式的定义也比较简单:一个类只能允许创建一个对象或者实例,那么这个类就是单例类,这种设计模式就叫做单例模式。

单例模式有哪些好处:

  • 类的创建,特别是一个大型的类,只创建一个类,避免内存和 CPU 的开销。
  • 降低内存使用,减少 GC 次数,避免 GC 的压力。
  • 避免对资源的重复请求。
  • 避免创建多个实例引起系统的混乱或者系统数据冲突。

ID 生成器单例类实战

单例模式,其中的 “例” 表示 “实例” ,一个类需要保证仅有一个实例,并提供一个访问它的全局访问点,实现一个单例,需要符合以下几点要求:

  • 构造函数需要设置 private 权限,避免外部通过 new 创建实例,通过一个静态方法给其他类获取实例。
  • 对象创建需要考虑线程安全问题。
  • 需要考虑延迟加载问题。
  • 外部类获取实例需要考虑性能方法。

在电商系统的订单模块,每次下单都需要生成新的订单号。就需要调用订单号生成器。

1、 创建一个简单单例类

public class SnGenerator {

    private AtomicLong id = new AtomicLong(0);
    // 创建一个 Singleton 对象
    private static SnGenerator instance = new SnGenerator();

    // 构造函数设置为 private,类就无法被实例化
    private SnGenerator() {}

    // 获取唯一实例
    public static SnGenerator getInstance() {
        return instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }
}

2、获取 Singleton 类的唯一实例

public class SingletonTest {

    public static void main(String[] args) {
        // 编译报错,因为 Singleton 构造函数是私有的
        //Singleton singleton = new Singleton();

        SnGenerator snGenerator = SnGenerator.getInstance();
        for (int i = 0; i < 10; i++) {
            System.out.println(snGenerator.getSn());
        }

    }
}

控制台输出生成的 id:

1
2
3
4
5
6
7
8
9
10

以上首先创建一个单例类,提供唯一的单例获取方法 getInstance。SingletonTest 类通过 Singleton.getInstance 获取实例,获取到实例,也就获取到实例所有的方法。示例中调用 getSn 方法,获取到唯一的订单号了。

饿汉单例

饿汉单例实现起来比较简单,所谓 “饿汉” 重点在饿,开始就需要创建单例。在类加载时,就创建好了实例。所以 instance 实例的创建是线程安全,不存在线程安全问题。但是这种方式不支持延迟加载,类加载时占用的内存就比较高。

public class SnGenerator {

    private AtomicLong id = new AtomicLong(0);

    private static SnGenerator instance = new SnGenerator();

    private SnGenerator() {}

    public static SnGenerator getInstance() {
        return instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }
}

饿汉单例解决线程安全问题,项目启动时就创建好了实例,就需要考虑创建和获取实例的线程安全问题。但是不支持延迟,如果实例的占用内存比较大,或者实例加载时间比较长,类加载的时候就创建实例,就比较浪费内存或者增加项目启动时间。

对饿汉单例来说,不支持延迟加载,确实是比较浪费内存。但是一个实例内存相对于一个 Java 项目内存占用影响是微乎其微。部署服务端项目时会分配几倍于项目启动占用的内存,所以饿汉单例占用内存还是可以接受的。而且如果占用内存比较大,初始化实例也可以发现内存不足的问题,并及时的处理。避免程序运行后,再去初始化实例,导致系统内存溢出,影响系统稳定性。

懒汉单例

既然饿汉单例单例不支持延迟加载,那我们就介绍一下支持延迟的加载的单例:懒汉单例。所谓"懒汉"重点在懒,一开始是不会初始化实例,而等到被调用才会初始化单例

public class LazySnGenerator {

    private AtomicLong id = new AtomicLong(0);

    private static LazySnGenerator instance;

    // 构造函数设置为 private,类就无法被实例化
    private LazySnGenerator() {}

    // 获取唯一实例
    public static LazySnGenerator getInstance() {
        if (instance == null) {
            instance = new LazySnGenerator();
        }
        return instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }

}

上面的懒汉单例最开始不会初始化实例,而且等到 getInstance 方法被调用时,才会时候实例,这样支持懒加载的方式,优点是不占内存。

但是懒汉单例缺点也比较明显,在多线程环境下,getInstance 方法不是线程安全的。

打个比方,多个线程同时执行到 if (instance == null)结果都为 true,进而都会创建实例,所以上面的懒汉单例不是线程安全的实例。

加同步锁的懒汉单例

懒汉单例存在多线程安全问题,第一想到的就是给 getInstance 添加同步锁,添加锁后,保证了线程的安全。


public class LazySnGenerator {

    private AtomicLong id = new AtomicLong(0);

    private static LazySnGenerator instance;

    // 构造函数设置为 private,类就无法被实例化
    private LazySnGenerator() {}

    // 获取唯一实例
    public synchronized static LazySnGenerator getInstance() {
        if (instance == null) {
            instance = new LazySnGenerator();
        }
        return instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }

}

添加同步锁后懒汉单例,并发量下降,如果方法被频繁使用,频繁的加锁、释放锁,有很大的性能瓶颈。

双重检验懒汉单例

饿汉单例不支持延迟加载,懒汉单例有性能问题,不支持高并发。就需要一种既支持延迟加载又支持高并发的单例,也就是双重检验懒汉单例。对上面的懒汉单例进行优化之后,得出如下代码。

public class LazyDoubleCheckSnGenerator {

    private AtomicLong id = new AtomicLong(0);

    private static LazyDoubleCheckSnGenerator instance;

    // 构造函数设置为 private,类就无法被实例化
    private LazyDoubleCheckSnGenerator() {}

    // 双重检测
    public static LazyDoubleCheckSnGenerator getInstance() {
        if (instance == null) {
            // 类级别锁
            synchronized (LazyDoubleCheckSnGenerator.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSnGenerator();
                }
            }
        }
        return instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }
}

双重检测首先判断实例是否为空,如果为空就使用类级别锁锁住整个类,其他线程也只能等待实例新建后,才能执行 synchronized 代码块的代码,而此时 instance 不为空,就不会继续新建实例。从而确保线程安全。getInstance 只会在最开始的时候,性能较差。创建实例之后,后面的线程都不会请求到 synchronized 代码块。后续并发性能也提高了。

CPU 指令重排可能会导致新建对象并赋值给 instance 之后,还来得及初始化,就会其他线程使用。导致系统报错,为了解决这个问题,就需要给 instance 成员变量添加 volatile 关键字禁止指令重排。

静态内部类单例

和双重检测单例一样,静态内部类既支持延迟加载又支持高并发。首先看一下代码实现。

public class SnStaticClass {
    private AtomicLong id = new AtomicLong(0);

    private static LazySnGenerator instance;

    // 构造函数设置为 private,类就无法被实例化
    private SnStaticClass() {}

    private static class SingletonHolder{
        private static final SnStaticClass instance = new SnStaticClass();
    }

    // 静态内部类获取实例
    public synchronized static SnStaticClass getInstance() {
        return SingletonHolder.instance;
    }

    public long getSn() {
        return id.incrementAndGet();
    }
}

SingletonHolder 是一个静态内部类,当 SnStaticClass 加载时,并不会加载 SingletonHolder 静态内部类,也就不会执行静态内部的代码。在类加载初始化阶段,不会执行静态内部类的代码。只有 getInstance 方法,执行 SingletonHolder 静态内部类,才会创建 SnStaticClass 实例。而 instance 创建的安全性,都是由 JVM 保证的。虚拟机使用加锁同步机制,保证实例只会创建一次。这种方式不仅实现延迟加载,也保证线程安全

枚举单例

枚举实例单例是一个简答实现方式,这种方式是通过 Java 枚举类性本身的特性,来保证实例的唯一和线程的安全。

public enum SnGeneratorEnum {

    instance;

    private AtomicLong id = new AtomicLong(0);

    public long getSn() {
        return id.incrementAndGet();
    }

}

单例模式的应用

在 Java 开发中,有很多地方使用到了单例模式。比如 JDK、Spring。

JDK

Runtime 类封装了 Java 运行信息,可以获取有关运行时环境的信息,每个 JVM 进程只有一个运行环境,只需要一个 Runtime 实例,所以 Runtime 一个单例实现。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ......省略
}

由以上代码可知,Runtime 是一个饿汉单例,类加载时就初始化了实例,提供 getRuntime 方法提交单例的调用。

Spring

大部分 Java 项目都是基于 Spring 框架开发的,Spring 中 bean 简单分成单例和多例,其中 bean 的单例实现既不是饿汉单例也不是懒汉单例。是基于单例注册表实现,注册表就就是一个哈希表,使用一个哈希表存储 bean 的信息。

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

singletonObjects 表示一个单例注册表,key 存储 bean 的名称,value 存储 bean 的实例信息。DefaultSingletonBeanRegistry 类的 getSingleton 方法实现 bean 单例,以下摘取主要的的代码。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(beanName, "Bean name must not be null");
        // 锁住注册表
		synchronized (this.singletonObjects) {
            // 获取 bean 信息,不存在就创建一个 bean
			Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
				
				beforeSingletonCreation(beanName);
				boolean newSingleton = false;
				boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
				
				try {
                    // 创建 bean
					singletonObject = singletonFactory.getObject();
					newSingleton = true;
				}
				catch (IllegalStateException ex) {
					
				}
				catch (BeanCreationException ex) {
					
				}
				finally {
					
				}
                // 创建好的 bean 存进 map 中
				if (newSingleton) {
					addSingleton(beanName, singletonObject);
				}
			}
			return singletonObject;
		}
	}

Spring 获取 bean,锁住整个注册表,首先从 map 中获取 bean,如果 bean 不存在,就创建一个 bean,并存入 map 中。后续获取 bean,获取到的都是 map 的 bean。并不会创建新的 bean。

总结

单例模式一个最简单的一种设计模式,该设计模式是一种创建型设计模式。规定了一个类只能创建一实例。很多类只需要一个实例,这样的好处,减少内存的占用和 CPU 的开销,减少 GC 的次数。同时也减少对资源的重复使用。

  • 以生成订单系统的订单号为例,分别介绍几种单例模式。
    • 饿汉单例:线程安全,但不支持延迟加载,不使用也会占用内存,比较浪费内存。但是类加载时创建实例,可以及时的发现内存不足问题。
    • 懒汉单例:支持延迟加载,但是线程不安全。多线程获取实例,可能会创建多个实例,就需要使用同步锁,锁住获取实例的方法,但是加了锁之后,性能就比较差。
    • 双重检测懒汉单例:针对上面不同同时满足延迟加载和线程安全问题,就设计出来双重检测的懒汉单例,主要将锁的代码块范围缩小,先获取实例,如果实例为空,才使用类级别锁,锁住代码,创建实例。当创建好实例后,后面请求都不会进同步锁的代码块,性能也不会降低。还需要考虑指令重排的问题,需要给成员变量添加 volatile 关键字禁止指令重排。
    • 静态内部类:也同时满足延迟加载和线程安全,延迟加载是在类加载时不会静态内部类的代码,只有调用时候才会执行静态内部类的代码。JVM 使用同步锁的机制保证获取实例是线程安全的。
    • 枚举单例:通过 Java 枚举类性本身的特性,来保证实例的唯一和线程的安全。
  • 单例模式的应用
    • JDK: Runtime 封装了 Java 运行信息,可以获取有关运行时环境的信息,一个 JVM 只需要一个 Runtime 实例.Runtime 单例是饿汉单例,在类加载时就初始化实例。
    • Spring: Spring 的 bean 支持单例,使用单例注册表,一种哈希表存储 bean信息,key 是存储 bean 的名称,value 是存储 bean 的实例。获取 bean 首先锁住表,然后获取 bean,如果为空就创建 bean,并存入表中,后续都能从哈希表中获取 bean 实例了。

参考

  • 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?

  • Spring学习之路——单例模式和多例模式

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

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

相关文章

Unity 之ToolTip的用法

文章目录 在Unity中&#xff0c;ToolTip是一个在编辑器中使用的UI元素&#xff0c;它提供了鼠标悬停在某个对象或控件上时显示的文本信息。ToolTip通常用于向开发人员提供有关对象、字段、控件或菜单项的附加信息&#xff0c;从而帮助他们更好地理解和使用这些元素。 ToolTip通…

细节揭示:XXE漏洞复现步骤及安全防护建议

环境准备 这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 攻击相关介绍 介绍&#xff1a; XXE漏洞发生在那些使用XML解析器处理用户提供的XML输入的应用程序中。攻击者通过在用户输入的XML文档中插入恶意的实体引用&#xff0c;…

gin框架

【狂神说】Gin框架一小时上手 | 快速转型GoWeb开发 | Go语言零基础教程_哔哩哔哩_bilibili 1.介绍 2.简单程序 1&#xff09;gin.GET/POST/PUT/DELETE函数 Go Gin 简明教程 | 快速入门 | 极客兔兔 (geektutu.com) 我的理解是&#xff1a;这类函数就像是在监听接口一样&…

深入理解搜索引擎优化(SEO)

深入理解搜索引擎优化 深入理解搜索引擎优化(SEO)1、SEO基础入门SEO概述搜索引擎营销策略SEO查询工具与站长平台收录与权重 2、SEO站内优化关键词域名、主机、程序与SEO设计技巧及优化标签优化技巧(TDK)页面关键词的布局和密度网站内部优化与代码优化301重定向&#xff0c;404优…

文件读取漏洞复现(Metinfo 6.0.0)

安装环境 安装phpstudy&#xff0c;下载MetInfo 6.0.0版本软件&#xff0c;复制到phpstudy目录下的www目录中。 打开phpstudy&#xff0c;访问浏览器127.0.0.1/MetInfo6.0.0/install/index.php&#xff0c;打开Meinfo 6.0.0主页&#xff1a; 点击下一步、下一步&#xff0c…

stencilJs学习之构建 Drawer 组件

前言 在之前的学习中&#xff0c;我们已经掌握了 stencilJs 中的一些核心概念和基础知识&#xff0c;如装饰器 Prop、State、Event、Listen、Method、Component 以及生命周期方法。这些知识是构建复杂组件和应用的基础&#xff0c;而抽屉组件是一个很好的示例&#xff0c;能够…

温室气体数据记录软件

温室气体数据记录软件用于记录温室气体分析仪、冷阱系统、阀箱以及采样单元数据的获取及记录。其软件界面如下&#xff1a; 在软件操作几面上部是工具栏&#xff0c;可以实现软件的各种操作&#xff0c;工具栏的排布如下所示&#xff1a; 最左侧为“连接”工具&#xff0c;用…

Android 蓝牙开发( 四 )

前言 上一篇文章给大家分享了Kotlin版的Android蓝牙的基础知识和基础用法&#xff0c;不过上一篇都是一些零散碎片化的程序&#xff0c;&#xff0c;这一篇给大家分享Android蓝牙开发实战项目KotlinCompose的初步使用 效果演示 : Android Compose 蓝牙开发 Android蓝牙实战开发…

upload-labs靶场通关详解

文章目录 Pass-01Pass-02Pass-03Pass-04Pass-05Pass-06Pass-07Pass-08Pass-09Pass-10Pass-11Pass-12Pass-13Pass-14Pass-15Pass-16Pass-17Pass-18Pass-19Pass-20方法一&#xff08;文件夹名欺骗绕过&#xff09;方法二&#xff08;%00截断攻击&#xff09; Pass-21 Pass-01 绕过…

Vert.x 源码解析(4.x)(一)——Context源码解析

目录 1.简介 Vert.x 中&#xff0c;多线程环境下的资源管理和状态维护是一个复杂的问题。为了解决这个问题&#xff0c;Vert.x 引入了 Context 这个核心概念。Context 负责在多线程环境下协调异步操作&#xff0c;提供线程安全的资源访问&#xff0c;并确保异步操作的正确执行…

Spring MVC工作流程

SpringMVC 的执行流程如下。 用户通过浏览器发起一个 HTTP 请求&#xff0c;该请求会被 DispatcherServlet&#xff08;前端控制器&#xff09;拦截&#xff1b;DispatcherServlet 调用 HandlerMapping&#xff08;处理器映射器&#xff09;找到具体的处理器&#xff08;Handl…

LinuxUbuntu安装OpenWAF

Linux&Ubuntu安装OpenWAF 官方GitHub地址 介绍 OpenWAF&#xff08;Web Application Firewall&#xff09;是一个开源的Web应用防火墙&#xff0c;用于保护Web应用程序免受各种网络攻击。它通过与Web服务器集成&#xff0c;监控和过滤对Web应用程序的流量&#xff0c;识…

基于移动端的校园失物招领系统 微信小程序的设计与实现779m5

于校园失物招领系统功能所牵扯的数据都是通过失主进行校园失物招领系统等相关的数据信息内容、并且可以实现首页、个人中心、失主管理、物品类型管理、失物展示管理、失物认领管理、在线投诉管理、论坛交流、系统管理等功能可以通过系统进行分配&#xff0c;传统的手工作业模式…

WebDAV之π-Disk派盘 + notototo

notototo是一款功能丰富的笔记软件,提供了多种功能,包括载入PDF文件并进行批注和标记的能力。您可以使用Apple Pencil或手指在PDF文件上进行写作和绘图操作。 同时,notototo也提供了与团队合作的功能,您可以连接到服务器并与他人协作。此外,您还可以在notototo中进行绘图,…

Dolphin for Mac(Wii游戏模拟器)配置指南

Wii模拟器Dolphin Mac是款适合Mac电脑中的游戏玩家们使用的模拟器工具。Wii模拟器Dolphin Mac官方版支持直接运行游戏镜像文件&#xff0c;玩家可以将游戏ISO拷贝到某一个文件夹中统一进行管理。Wii模拟器Dolphin Mac除了键盘和鼠标外&#xff0c;还支持配合原版的Wii遥控器操作…

MySQL告警“Connection attributes of length 570 were truncated“

mysql的错误日志中看到如下报错"[Warning] Connection attributes of length 571 were truncated"。比如&#xff1a; 2023-09-01T08:37:49.87392408:00 9149015 [Warning] [MY-010288] [Server] Connection attributes of length 570 were truncated (76 bytes los…

ip route get ip地址 应用案例

应用场景 在做虚拟化实验用的虚拟机和实际的ECS云主机一般都会有多个网卡&#xff0c;网络的联通性是经常碰到的问题。比如在一个VM上有3个网卡&#xff0c;分别为ens160(和寄主机进行桥接的网卡10.0.0.128)、ens224&#xff08;连接仅主机网络10.0.0.0/24的网卡10.0.0.128&…

三维模型OBJ格式轻量化顶点压缩主要技术方法分析

三维模型OBJ格式轻量化顶点压缩主要技术方法分析 三维模型的OBJ格式轻量化中&#xff0c;顶点压缩是一项重要的技术方法&#xff0c;用于减小模型文件的大小。以下是关于三维模型OBJ格式轻量化顶点压缩的主要技术方法的分析&#xff1a; 1、顶点位置量化&#xff1a; 顶点位置…

直播平台源码弹性云托管技术:稳定直播与降低成本的利器

在当今的互联网时代&#xff0c;直播平台源码层出不穷&#xff0c;直播平台源码不仅可以让人们获取最新的资讯、查找资料等信息获取&#xff0c;还能让人们在其中观看短视频、直播、与其他人聊天等互动放松&#xff0c;直播平台源码的受欢迎与平台人数的增加使得人们在选择直播…

Python爬虫(十七)_糗事百科案例

糗事百科实例 爬取糗事百科段子&#xff0c;假设页面的URL是: http://www.qiushibaike.com/8hr/page/1 要求&#xff1a; 使用requests获取页面信息&#xff0c;用XPath/re做数据提取获取每个帖子里的用户头像连接、用户姓名、段子内容、点赞次数和评论次数保存到json文件内…