Java设计模式-单例模式最佳实践

news2024/11/15 13:35:48

在这里插入图片描述

1. 单例模式简介

Java 单例模式是四大设计模式之一,属于创建型设计模式。从定义上看,它似乎是一种简单的设计模式,但在实现时,如若不注意,它会带来很多问题。

在本文中,我们将了解单例设计模式原则,探索实现单例设计模式的不同方法,以及一些最佳实践。

2. 单例模式原理

  • 单例模式限制类的实例化,并确保 Java 虚拟机中只存在该类的一个实例。
  • 单例类必须提供一个全局访问点来获取该类的实例。
  • 单例模式用于日志记录驱动程序对象缓存线程池
  • 单例设计模式也用于其他设计模式,如抽象工厂生成器原型外观等。
  • 单例设计模式也用于核心 Java 类(例如,java.lang.Runtimejava.awt.Desktop

3. 单例模式实现方式

为了实现单例模式,我们有不同的方法,但它们都有以下共同的概念

  • 私有构造函数用于限制其他类对该类的实例化。
  • 同一类的私有静态变量,是该类的唯一实例
  • 返回类的实例的公共静态方法,这是外界获取单例类实例的全局访问点

在这里插入图片描述

3.1 急切初始化

急切初始化中,单例类的实例是在类加载时创建的。急切初始化的缺点是即使客户端应用程序可能没有使用该方法,也会创建该实例。以下是静态初始化单例类的实现:

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

如果您的单例类没有使用大量资源,则可以使用这种方法。但在大多数情况下,单例类是为文件系统、数据库连接等资源创建的。除非客户端调用该getInstance方法,否则我们应该避免实例化。此外,此方法不提供任何异常处理选项。

3.2 静态块初始化

静态块初始化实现与急切初始化类似,不同之处在于类的实例是在静态块中创建的,并提供了异常处理的选项。

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

注意:立即初始化和静态块初始化都会在使用之前创建实例,这不是最佳做法

3.3 延迟初始化

实现单例模式的惰性初始化方法在全局访问方法中创建实例。以下是使用此方法创建单例类的示例代码:

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

上述实现在单线程环境中运行良好,但在多线程系统中,如果if同时有多个线程处于条件中,则可能会导致问题。它将破坏单例模式,并且两个线程将获得单例类的不同实例。在下一节中,我们将看到创建线程安全的单例类的不同方法。

3.4 线程安全单例

在这里插入图片描述
创建线程安全的单例类的一个简单方法是将全局访问方法同步,以便一次只有一个线程可以执行此方法。以下是此方法的一般实现:

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

上述实现工作正常,并且提供了线程安全性,但由于与同步方法相关的成本,它降低了性能,尽管我们只需要在可能创建单独实例的前几个线程中使用该方法。为了避免每次都产生这种额外的开销,使用了双重检查锁定if原则。在这种方法中,同步块用于条件中,并进行额外检查以确保只创建了一个单例类的实例。以下代码片段提供了双重检查锁定实现

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

以上代码的同步锁,仅仅加到了if (instance == null) {这个判断下面,也就是只有当实例未初始化时,才到这里面同步去初始化,也就是并发只有在未初始化那一短暂时刻才会发生,极大降低了同步锁带来的并发开销!视为最佳方式!

3.5 Bill Pugh 单例实现

在 Java 5 之前,Java 内存模型存在很多问题以前的方法在某些情况下会失败,因为太多线程同时尝试获取单例类的实例。因此,Bill Pugh提出了一种不同的方法,使用内部静态辅助类来创建单例类。以下是 Bill Pugh Singleton 实现的一个示例:

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

注意包含单例类实例的私有内部静态类。加载单例类时,SingletonHelper该类不会加载到内存中,只有当有人调用该getInstance()方法时,才会加载该类并创建单例类实例。这是单例类最广泛使用的方法,因为它不需要同步。

3.6 使用反射来破坏单例模式

反射可以用来破坏之前所有的单例实现方式。下面是一个示例类:

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // This code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
}

运行上述测试类时,您会注意到hashCode两个实例并不相同,这会破坏单例模式。反射非常强大,在 Spring 和 Hibernate 等许多框架中都有使用。继续学习Java 反射教程。

3.7 枚举单例

为了通过反射克服这种情况,Joshua Bloch建议使用 来enum实现单例设计模式,因为 Java 确保任何enum值在 Java 程序中只实例化一次。由于Java 枚举值是全局可访问的,因此单例也是如此。缺点是该enum类型有些不灵活(例如,它不允许延迟初始化)。

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // do something
    }
}

3.8 序列化和单例

有时在分布式系统中,我们需要Serializable在单例类中实现接口,以便我们可以将其状态存储在文件系统中并在以后的某个时间点检索它。这是一个Serializable也实现接口的小单例类:

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }
}

序列化单例类的问题在于,每当我们反序列化它时,它都会创建该类的新实例。以下是一个例子:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // deserialize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }
}
Output
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

所以它破坏了单例模式。为了克服这种情况,我们需要做的就是提供readResolve()方法的实现。

protected Object readResolve() {
    return getInstance();
}

此后,您会注意到hashCode测试程序中的两个实例是相同的

4. 总结

好啦,以上就是关于java设计模式的单例模式详细介绍,整理出八种初始化方式及其利弊分析,最佳方式推荐是3.4章节的线程安全单例

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

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

相关文章

使用 GPU 加速的 XGBoost 预测出租车费用

目录 XGBoost GPU 加速的 XGBoost 用例数据集示例 将文件中的数据加载到 DataFrame 定义特征数组 保存模型 总结 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家, 可以当故事来看&#xf…

小智纯前端js报表实战5-绝对坐标-横向扩展

绝对坐标-横向扩展 概述 绝对坐标-横向扩展:绝对坐标定位 层次坐标是实现复杂报表的一个重要功能。 在进行小智报表模板设计时,单元格尚未进行扩展,但是有些时候需要获取扩展后的单元格并进行计算。例如,A1单元格扩展成A1-D1&am…

VUE+Spring前后台传值的坑,后台接收的String参数在末尾会出现 “=”

一、问题 VUESpringBoot做增删改查时,前端使用axios.post发起请求,传输主键字符型参数 taskId 到后台,后台再进行删除处理。 实际过程中发现后台拿到的数据再末尾多了一个等号,但是通过console.log(taskId)前台打印参数是正常的…

巴洛克风格的现代演绎,戴上亚法银耳机,感受古典雕花与现代声学的碰撞

flipears耳机品牌以其独特的风格、精细的配置和卓越的音质在耳机市场中很受欢迎,像是我最近用过的一款Artha Argentum亚法银,就采用了纯银外壳,而且用料扎实,具有出众的声学表现,带来了更干净清澈的声底。内在配置方面…

[Linux] LVM挂载的硬盘重启就掉的问题解决

问题:系统重启后挂在逻辑卷的盘会掉(必现) 环境:SUSE Linux 11 SP4 原因:boot.lvm是关闭的 解决:boot.lvm设置开启 参考资料: linux下lvm状态Not avaliable问题排查及处理(常见Suse操作系统…

使用ubuntu串口数据收和发不一致问题

串口配置 使用virtual Serial Port Driver Pro模拟串口两个串口,com2和com3,使用默认配置;通过virtual box 串口映射功能,在Ubuntu里使用CuteCom打开com2接受和发送数据,在windows里使用com3发送和接收数据。 遇到问…

24/8/9算法笔记 随机森林

"极限森林"(Extremely Randomized Trees,简称ERT)是一种集成学习方法,它属于决策树的变体,通常被归类为随机森林(Random Forest)的一种。极限森林的核心思想是在构建决策树时引入极端…

空间推理验证码的介绍!

空间推理验证码 ​是一种验证码形式,‌旨在通过要求用户解决一些视觉或空间推理问题来区分计算机和人类用户。‌这种验证码形式要求用户通过完成一些视觉或空间推理任务来证明他们是真实的人类用户,‌而不是计算机程序。‌空间推理验证码通常涉及一些图…

智慧交通:将物联网与人工智能完美融合

智慧交通是当今社会面临的一个重要挑战,也是人们生活质量提高的一个重要方面。通过将物联网技术与人工智能相结合,我们能够实现智慧交通系统的全面升级和优化,为人们带来更加便捷、高效和安全的出行体验。 在智慧交通领域,物联网…

Java面试题--JVM大厂篇之从原理到实践:JVM 字节码优化秘籍

目录 引言: 正文: 1. JVM 字节码生成原理 2. 字节码优化的痛点 3. 字节码优化策略 3.1 方法内联(Method Inlining) 3.2 循环展开(Loop Unrolling) 3.3 常量折叠(Constant Folding&#…

线程池原理(二)关键源码剖析

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 属性 & 构造方法 对于 ThreadPoolExecutor 有几个关键的属性,这里需要先大致了解: public class ThreadPoolExecutor extends AbstractExecutorService {// 控制变量-存放状态和…

什么是NLP分词(Tokenization)

在自然语言处理和机器学习的领域里,咱们得聊聊一个超基础的技巧——就是“分词”啦。这个技巧啊,就是把一长串的文字切分成小块,让机器能更容易地“消化”。这些小块,不管大小,单个的字符也好,整个的单词也…

汽车EDI:德国大众 EDI 项目案例

德国大众(Volkswagen)是成立于1937年的大型汽车制造商,总部位于德国沃尔夫斯堡。大众集团拥有众多知名汽车品牌,如奥迪、保时捷、宾利、兰博基尼、布加迪等,业务遍及全球。作为一个全球性企业,大众集团依赖…

时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention

时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention 文章目录 前言时序预测|基于变分模态分解-时域卷积-双向长短期记忆-注意力机制多变量时间序列预测VMD-TCN-BiLSTM-Attention 一、VMD-TCN-BiLSTM-Attention模型1. **…

SystemUI plugin 开发

一、前言 SystemUI结构复杂,模块数量众多,最重要的是SystemUI属于常驻进程是一个系统的门面,且不能自升级,如果定制功能对主项目做复杂的修改,首先会造成适配压力,如果对主框架不甚理解,有可能会造成很多隐藏的Bug,且不易修复,一旦崩溃对整个系统的影响很大,那么怎…

【从零开始一步步学习VSOA开发】VSOA数据流

VSOA数据流 概念 实际业务中常常存在既有实时命令通信,又有非实时的大数据通信,如文件、音视频传输服务等,如果使用常规的 RPC 或订阅/发布功能来实现,将实时命令和大数据传输混在一起,则会影响 RPC 通道响应的实时性…

C语言程序设计-[10] for语句循环结构

1、for语句循环结构定义 for语句循环结构的一般形式、流程图和执行过程如下: ​ 注1:计算表达式2是循环的判定表达式。与前面一样,这个表达式可以是任意的,只要有值就行,遵循非0即真的原则。 注2:一个循…

(源码)Springboot项目集成Activiti工作流,前端Vue,Bpmn.js

前言 activiti工作流引擎项目,企业erp、oa、hr、crm等企事业办公系统轻松落地,一套完整并且实际运用在多套项目中的案例,满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器,流行的前后端…

【Python】nn.nn.CircularPad1、2、3d函数和nn.ConstantPad1、2、3d函数详解和示例

前言 在深度学习中,尤其是在处理图像、音频或其他多维数据时,数据填充(Padding)是一个常见的操作。填充不仅可以保持数据的空间维度,还能在卷积操作中避免信息丢失。PyTorch提供了多种填充方式,其中nn.Cir…

unity 本地使用Json(全套)

提示:文章有错误的地方,还望诸位大神不吝指教! 文章目录 前言一、Json是什么?二、创建Json文件1.在线编辑并转实体类(C#)2.Json文件 三、解析Json并使用四、报错:JsonError:JsonExce…