单例模式:确保一个类只有一个实例

server/2025/3/11 4:43:02/

目录

引言

1. 单例模式的核心思想

2. 单例模式的实现方式

2.1 饿汉式单例

2.2 懒汉式单例

2.3 线程安全的懒汉式单例

2.4 双重检查锁定(Double-Checked Locking)

2.5 静态内部类实现单例

2.6 枚举实现单例

3. 单例模式的使用场景

4. 单例模式的优缺点

优点:

缺点:

5. 总结


引言

单例模式(Singleton Pattern)是设计模式中最简单且最常用的创建型模式之一。它的核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式在许多场景中非常有用,例如配置管理、线程池、数据库连接池等。


1. 单例模式的核心思想

单例模式的核心思想是:

  1. 私有化构造函数:防止外部通过 new 关键字创建实例。

  2. 提供一个静态方法:用于获取类的唯一实例。

  3. 确保唯一性:在整个应用程序生命周期中,类的实例只有一个。


2. 单例模式的实现方式

单例模式有多种实现方式,每种方式都有其优缺点。以下是几种常见的实现方式:

2.1 饿汉式单例

饿汉式单例在类加载时就创建实例,因此是线程安全的。

public class Singleton {// 在类加载时创建实例private static final Singleton INSTANCE = new Singleton();// 私有化构造函数private Singleton() {}// 提供全局访问点public static Singleton getInstance() {return INSTANCE;}
}

饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长的时间里,我都是饿汉模式来完成单例 的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:

 public class Hungry {private byte[] data1 = new byte[1024];private byte[] data2 = new byte[1024];private byte[] data3 = new byte[1024];private byte[] data4 = new byte[1024];private Hungry() {}private final static Hungry hungry = new Hungry();public static Hungry getInstance() {return hungry;}}

 在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如 果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用 到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模 式:懒汉式。

优点

  • 实现简单,线程安全。

缺点

  • 如果实例未被使用,会造成资源浪费。


2.2 懒汉式单例

懒汉式单例在第一次调用 getInstance() 时才创建实例。

public class LazyMan {private LazyMan() {System.out.println(Thread.currentThread().getName()+"Start");}private static LazyMan lazyMan;public static LazyMan getInstance() {if (lazyMan == null) {lazyMan = new LazyMan();}return lazyMan;}// 测试并发环境,发现单例失效public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(()->{LazyMan.getInstance();}).start();}}}

缺点

  • 线程不安全,多个线程可能同时进入 if (instance == null) 条件,导致创建多个实例。


2.3 线程安全的懒汉式单例

通过在 getInstance() 方法上加锁,可以解决懒汉式单例的线程安全问题。

public class LazyMan {private LazyMan() {}private static LazyMan lazyMan;public static LazyMan getInstance() {if (lazyMan == null) {synchronized (LazyMan.class) {if (lazyMan == null) {lazyMan = new LazyMan();}}}return lazyMan;}}

保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用 效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为 :

lazyMan = new LazyMan();

不是原子性操作,至少会经过三个步骤:

1. 分配对象内存空间

2. 执行构造方法初始化对象

3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第 二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回 的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错 误,所以就有了下面一种单例模式


2.4 双重检查锁定(Double-Checked Locking)

双重检查锁定是一种优化后的线程安全懒汉式单例实现方式。

这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

 public class LazyMan {private LazyMan() {}private volatile static LazyMan lazyMan;public static LazyMan getInstance() {if (lazyMan == null) {synchronized (LazyMan.class) {if (lazyMan == null) {lazyMan = new LazyMan();}}}return lazyMan;}
}

优点

  • 线程安全,且只有在第一次创建实例时加锁,性能较好。

注意

  • 必须使用 volatile 关键字,防止指令重排序导致的问题。


2.5 静态内部类实现单例

静态内部类实现单例是一种优雅且线程安全的方式。

public class Singleton {private Singleton() {}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}

优点

  • 线程安全,且只有在调用 getInstance() 时才会加载 SingletonHolder 类,实现懒加载。


2.6 枚举实现单例

枚举实现单例是《Effective Java》推荐的方式,它天然支持线程安全和防止反射攻击。

public enum Singleton {INSTANCE;public void doSomething() {System.out.println("Doing something...");}
}

优点

  • 线程安全,代码简洁,防止反射和序列化破坏单例。


3. 单例模式的使用场景

单例模式适用于以下场景:

  1. 全局配置管理:例如读取配置文件,确保配置信息全局唯一。

  2. 数据库连接池:确保连接池只有一个实例,避免资源浪费。

  3. 日志管理:确保日志记录器全局唯一。

  4. 线程池:确保线程池的唯一性,避免重复创建线程。


4. 单例模式的优缺点

优点:

  • 节省资源:避免重复创建对象,减少内存开销。

  • 全局访问点:方便对唯一实例的管理和访问。

缺点:

  • 扩展性差:单例类通常难以扩展,因为其构造函数是私有的。

  • 违背单一职责原则:单例类既负责创建实例,又负责业务逻辑。

  • 测试困难:单例类的全局状态可能导致测试困难。


5. 总结

单例模式是一种简单但强大的设计模式,适用于需要全局唯一实例的场景。通过不同的实现方式(如饿汉式、懒汉式、双重检查锁定、静态内部类、枚举等),可以满足不同的需求。

在实际开发中,应根据具体场景选择合适的单例实现方式。如果需要懒加载且线程安全,推荐使用静态内部类或枚举实现;如果需要更高的性能,可以考虑双重检查锁定。


http://www.ppmy.cn/server/174095.html

相关文章

Vue 系列之:Vuex 和 Pinia

前言 每一个 Vuex 应用的核心就是 store&#xff08;仓库&#xff09;。“store”基本上就是一个容器&#xff0c;它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同&#xff1a; Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时…

数据分析和可视化课程实验报告一(数据分析基础)

实验题目&#xff1a; 数据分析基础 实验目的&#xff1a;1.掌握 Anaconda与 Jupyter Notebook的安装与使用方法 2.掌握 NumPy 创建数组、矩阵的方法以及常用的方法 3.掌握 NumPy 统计分析方法 …

C++ 滑动窗口

前言 C 中滑动窗口分两种&#xff0c;一种是给定窗口长度&#xff0c;还有一种是不定长窗口长度。 本篇文章主要讲解这两种状态的滑动窗口&#xff0c;结合例题让读者更好的理解 一、给定窗口长度K 一般的&#xff0c;对于给定窗口长度的题&#xff0c;通常要求我们对窗口内…

新型神经网络KAN:准确性高且易于解释

人工神经网络&#xff08;ANN&#xff09;是现代人工智能的核心技术&#xff0c;广泛应用于聊天机器人、图像生成器等领域。然而&#xff0c;传统的神经网络由于其复杂的结构和大量的神经元连接&#xff0c;往往被视为“黑匣子”&#xff0c;难以解释其内部工作原理。近年来&am…

nginx配置反向代理服务器,实现在https网站中请求http资源

文章目录 一、前言二、Nginx反向代理的工作原理三、Nginx反向代理的主要功能‌四、Nginx反向代理的配置和使用场景五、实战配置5.1 首先&#xff0c;修改宝塔面板配置5.2 接着配置代理服务器5.3 完成上面所有配置后5.4最后还要在原来的index.html文件里添加5.5 或者可以操作服务…

Map<String,Object>转换为XML报文的方法

1. 使用 JAXB (Java Architecture for XML Binding) JAXB 是 Java 标准库的一部分&#xff0c;支持将 Java 对象与 XML 相互转换。虽然 JAXB 主要用于将 Java 对象转换为 XML&#xff0c;但可以通过一些技巧将 Map<String, Object> 转换为 XML。 示例代码 import java…

firewalld富规则配置黑名单

1. 屏蔽指定 IP 地址 firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=192.168.1.100 reject"参数说明 --permanent:将规则永久保存,重启后仍然生效。--add-rich-rule:添加一条富规则。rule family=ipv4:指定规则适用于 IPv4 地址。…

可视化大屏出圈密码:地图组件深度解析

01 组件介绍 在数据可视化大屏搭建中&#xff0c;地图组件是使用最频繁且重要的组件之一。它不但能直观展现地理方位相关信息&#xff0c;还能通过与标记点、飞线、图表等各类组件巧妙搭配&#xff0c;达成更加多元、生动的数据呈现效果&#xff0c;在众多行业领域都得到了广泛…