多线程案例——单例模式

一、单例模式

1、概念

单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制实例数目、节省系统资源或确保全局一致性的场景中非常有用。

2、使用场景

配置文件管理器:整个应用只需要一个配置管理器

日志记录器:所有日志应该通过同一个日志对象记录

数据库连接池:管理数据库连接通常只需要一个池

线程池:系统通常只需要一个线程池管理器

二、实现方式

单例模式的实现方式有很多种,最常见的就是"饿汉"和"懒汉"两种模式。

1、饿汉模式

(1)饿汉模式即在类加载的同时创建实例

java 复制代码
public class EagerSingleton {
    // 类加载时就创建实例
    private static Singleton instance = new Singleton();
    
    // 私有构造函数防止外部实例化
    private Singleton() {}
    
    // 提供全局访问点
    public static Singleton getInstance() {
        return instance;
    }
}

(2)优缺点对比

优点:实现简单,线程安全

缺点:即使不使用也会创建实例,可能浪费资源

(3)为什么线程安全

静态变量的初始化时机:

静态变量 instance 在类加载时由 JVM 完成初始化,JVM 在加载类时会自动加锁,确保一个类只被加载一次,静态变量也只初始化一次。

无竞态条件:

实例的创建发生在类加载时,此时程序尚未进入多线程环境。

调用 getInstance() 时直接返回已初始化的静态变量,无需检查或同步。

2、懒汉模式------单线程版

和饿汉模式相对,懒汉模式是延迟创建实例,第一次使用的时候创建,我们来看代码示例:

(1)代码

java 复制代码
class Singleton { 
    private static Singleton instance = null;

    private Singleton() {}

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

(2)优缺点对比

优点:使用时才创建实例

缺点:每次获取实例都需要同步,性能较低

(3)线程安全问题

通过分析上面的过程,我们可以看出在if条件语句与new实例这一块存在问题(因为这是两个不同的操作,中间可能会被其他线程打断),具体表现:

假设线程A和线程B同时调用getInstance():

线程A执行到检查点(if (instance == null)),发现instance为null

线程B也同时执行到检查点,同样发现instance为null

线程A通过检查,进入创建点,开始创建实例

线程B也通过检查,进入创建点,也开始创建实例

最终会创建两个不同的实例,破坏了单例模式的基本原则

那么针对这样的问题,我们便可以采用加锁的手段进行处理,这样就得出多线程版本。

3、懒汉模式------多线程版本

(1)主要是针对上面线程不安全问题进行加锁处理,代码如下:

java 复制代码
class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

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

(2)线程安全分析:

getInstance() 被声明为 synchronized,确保同一时间只有一个线程能进入该方法。

线程 A 进入 getInstance(),检查 instance == null,创建实例。

线程 B 必须等待线程 A 释放锁后才能进入,此时 instance 已经初始化,直接返回已有实例。

(3)其他问题分析

对单线程版本加锁解决了线程安全问题,但是每次调用 getInstance() 都要获取锁,即使实例已经创建,仍然会有锁竞争,影响并发性能。

为了解决加锁引入的新问题,我们可以考虑按需加锁,真正涉及到线程安全的时候加锁,不涉及的时候不加锁,即双重检查锁。

(4)双重检查锁代码

java 复制代码
class Singleton {
    private static Singleton instance=null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查(无锁)
            synchronized (Singleton.class) {         // 同步块
                if (instance == null) {             // 第二次检查(有锁)
                    instance = new Singleton();     // 安全初始化
                }
            }
        }
        return instance;
    }
}

上面代码中两个if条件语句虽然一样,但是他们的作用却不同,第一个if是判断是否需要加锁,如果实例已经创建,就不设计线程安全问题,不需要在加锁了;第二个if是判断是否需要new对象,防止重复实例化。

两个检查协作具体表现是:

1、线程A和线程B同时调用 getInstance()

2、都通过外层检查(此时 instance 确实为 null)

3、线程A先获取锁,进入同步块:

执行内层检查(仍为 null)

创建实例

释放锁

4、线程B获取锁,进入同步块:

执行内层检查(此时 instance 已不为 null)

直接返回已有实例

5、后续所有线程调用时:

外层检查直接返回实例

完全不会进入同步块

我们在仔细分析上面代码,会发现还会有别的问题,那就是主要是指令重排序导致的可见性问题,具体来看:

在Java内存模型中,instance = new Singleton()这行代码实际上包含3个步骤:

1、分配对象内存空间

2、初始化对象(调用构造函数)

3、将instance引用指向该内存地址

由于指令重排序,JVM可能会优化为1→3→2的顺序执行。这样会导致:

线程A执行到步骤3时,instance已不为null,但对象还未初始化

线程B在第一次检查时发现instance不为null,直接返回未初始化完成的对象

那么针对指令重排序这个问题,我们可以使用volatile这个关键字来处理。

(5)改进代码

java 复制代码
class Singleton {
    private static volatile Singleton instance=null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查(无锁)
            synchronized (Singleton.class) {         // 同步块
                if (instance == null) {             // 第二次检查(有锁)
                    instance = new Singleton();     // 安全初始化
                }
            }
        }
        return instance;
    }
}

volatile的作用

禁止指令重排序:确保对象的初始化顺序为1→2→3

保证可见性:一个线程对instance的修改会立即对其他线程可见

内存屏障:防止读写操作越过屏障,确保操作顺序

通过上面多次的改进,我们解决了线程安全问题和指令重排序问题。

三、总结

单例模式是一种确保类只有一个实例并提供全局访问点的设计模式,适用于需要全局唯一对象的场景如配置管理、日志系统等。主要实现方式包括:饿汉式(类加载时创建实例,简单线程安全但可能浪费资源)、懒汉式(延迟加载但需处理线程安全)、双重检查锁(结合懒加载与线程安全,需volatile防止指令重排序)。选择实现时需权衡线程安全、性能与资源开销问题。

相关推荐
Bug退退退1233 分钟前
RabbitMQ 高级特性之消息确认
java·分布式·rabbitmq
The_cute_cat13 分钟前
JavaScript的初步学习
开发语言·javascript·学习
Naiva34 分钟前
【小技巧】Python + PyCharm 小智AI配置MCP接入点使用说明(内测)( PyInstaller打包成 .exe 可执行文件)
开发语言·python·pycharm
云动雨颤38 分钟前
Java并发性能优化|读写锁与互斥锁解析
java
梦子要转行43 分钟前
matlab/Simulink-全套50个汽车性能建模与仿真源码模型9
开发语言·matlab·汽车
ldj20201 小时前
Centos 安装Jenkins
java·linux
hqxstudying1 小时前
Intellij IDEA中Maven的使用
java·maven·intellij-idea
SimonKing1 小时前
拯救大文件上传:一文彻底彻底搞懂秒传、断点续传以及分片上传
java·后端·架构
深栈解码1 小时前
JUC并发编程 内存布局和对象头
java·后端
北方有星辰zz1 小时前
数据结构:栈
java·开发语言·数据结构