Java的单例模式

单例模式

单例模式是什么

单例模式(Singleton Pattern)是创建型设计模式的核心之一,其核心目标是:保证一个类在整个应用程序生命周期中仅有一个实例对象,并提供一个全局统一的访问入口来获取这个实例。

要实现单例,必须满足这三个关键约束:

私有化构造方法:禁止外部通过 new 关键字创建类的实例(从根源上避免多实例);

类内部维护唯一实例:通过静态变量存储这个唯一实例;

提供公共静态方法:作为全局访问点,返回这个唯一实例。

常见实现方式

单例的实现分 "饿汉式"(立即加载)和 "懒汉式"(延迟加载)两大类,不同方式的核心差异是实例初始化时机和线程安全性。

懒汉式单例

核心定义:懒汉式单例是单例模式的延迟加载实现范式,其核心特征为按需实例化。

核心特点:

  • 实例化时机:遵循延迟初始化原则,不会在类加载阶段创建实例,仅当外部首次调用获取实例的公共方法时,才触发实例的创建;若该实例始终未被调用,则不会完成初始化。
  • 线程安全约束:在多线程并发场景下,基础版懒汉式存在线程安全风险 ------ 若多个线程同时调用获取实例的方法,且此时实例尚未初始化,会导致多个线程同时进入实例未创建的判断逻辑,进而重复创建实例,破坏单例模式实例唯一性的核心约束。
  • 并发解决方案:为保障并发场景下的单例唯一性,需通过加锁机制(如synchronized关键字)解决并发冲突,常见方案包括对获取实例的方法整体加锁、双重检查锁等;加锁虽能保证线程安全,但会引入一定的性能开销。
  • 设计本质:属于 "时间换空间" 的设计方案 ------ 通过延迟实例化节省了类加载阶段的内存资源,但需在首次获取实例时承担实例化的时间成本,且并发场景下的加锁操作会额外增加时间开销。

传统实现代码:

java 复制代码
public class Singleton {
    //设置私有构造方法
    private Singleton(){
    }
    //声明一个Singleton对象为obj
    private static Singleton obj;
    //加锁保证obj只能实例化一次
    public static synchronized Singleton getInstance(){
        if(obj == null){
            obj = new Singleton();
        }
        return obj;
    }
}
  1. 性能问题
    将 synchronized 关键字直接修饰获取实例的 getInstance() 方法。该实现虽能保证线程安全,但存在显著的性能缺陷,无论单例实例是否已创建,每次调用 getInstance() 方法时,所有线程都需竞争 Singleton.class 对应的类锁,强制串行化执行方法逻辑。即便实例早已初始化完成,线程仍需经历加锁 / 解锁的开销,在高并发场景下会大幅降低方法调用效率。
  2. 锁粒度优化逻辑
    加锁的核心目标是防止实例未初始化时,多线程并发调用 getInstance() 导致重复创建实例,而非对整个方法的执行过程做串行化控制。因此,优化的核心思路是缩小锁粒度:仅对实例化对象的核心代码块加锁,而非修饰整个 getInstance() 方法。

优化实现代码:

java 复制代码
public class Singleton {
    //设置私有构造方法
    private Singleton(){
    }
    private volatile static Singleton  obj;
    //获取实例对象的方法
    public static Singleton getInstance(){
        //如果已有实例则直接返回,不走锁
        if(obj==null){
            //仅在没生成实例时加锁控制,使并发访问串行化
            synchronized(Singleton.class){
                //多个线程会按序执行到此处,需要再次检查是否已经实例化
                if(obj==null){
                    obj = new Singleton();
                }
            }
        }
        return  obj;
    }
}
  • 第一次判空(if(obj==null)):若实例已完成初始化,直接返回实例,规避锁竞争的开销,确保高并发场景下获取实例的性能;
  • 类级锁控制(synchronized(Singleton.class)):仅当实例未初始化时,才对类对象加锁,将并发线程的实例化操作串行化,从根源避免多线程重复创建实例;
  • 第二次判空(锁内if(obj==null)):防止多个等待锁的线程在锁释放后重复执行实例化逻辑(例如线程 A 获取锁并创建实例,线程 B 等待锁后若不二次检查,会再次创建实例);

volatile 关键字的核心作用:修饰实例变量obj可防止 JVM 对 obj = new Singleton() 执行指令重排。

该语句实际分为「分配内存→初始化对象→引用赋值」三步,若未加 volatile,可能出现其他线程获取到已赋值但未完成初始化的不完整实例,是 DCL 实现真正线程安全的必要条件。

饿汉式单例

饿汉式单例是单例模式的立即加载实现范式,核心特征为在类加载阶段完成唯一实例的创建,依托 JVM 原生的类加载机制保证线程安全。

核心特点:

  • 实例化时机:初始化饿汉式单例的实例化发生在 JVM 加载该类的初始化阶段(执行<clinit>()类初始化方法)。只要 JVM 首次加载该类,就会立即执行静态实例变量的赋值逻辑,完成单例对象的创建。
  • 线程安全:同一个类的<clinit>()方法在多线程环境下只会被执行一次(由 JVM 的类加载器加锁保证)。因此饿汉式单例无需通过 synchronized 等锁机制,天然避免了多线程并发创建多个实例的问题,线程安全性是原生级的,无并发风险。
  • 设计本质:空间换时间的权衡策略
    空间成本:在类加载阶段即完成单例实例的初始化并占用堆内存资源,若该实例全程未被业务逻辑调用,会造成内存资源的无效占用(无法通过常规手段释放);
    时间收益:后续所有获取实例的操作,均为无锁化的直接返回已初始化实例,既无需执行实例化逻辑的时间成本,也无任何并发同步开销,在高并发场景下能实现实例获取性能的最优解。

传统实现代码:

java 复制代码
public class Singleton {
    //私有构造方法
    private Singleton(){
    };
    //类加载时就实例化对象 加static
    private static Singleton obj=new Singleton();

    public static Singleton getInstance(){
        return  obj;
    }
}

核心问题:

上述实现中,通过static关键字将实例变量声明为类级静态成员,依据 JVM 类加载机制,该实例会在类初始化阶段完成创建。当外部调用该类的任意静态方法、访问该类的静态成员变量,甚至仅通过Class.forName()加载该类时,都会触发类初始化,进而提前创建单例实例。这会导致核心问题:若业务仅需调用该类的其他静态方法(无需使用单例实例),单例对象仍会被强制实例化并占用内存,造成内存资源的无效占用。

优化思路与原理:

为解决非按需初始化的资源浪费问题,同时保留其 JVM 原生线程安全的优势,可采用静态内部类(Holder 模式) 实现单例,核心原理依托 JVM 的类加载特性:

JVM 加载外部类时,不会主动加载其静态内部类,仅当外部代码首次访问静态内部类的静态成员时,才会触发内部类的加载与初始化;静态内部类的初始化过程由 JVM 保证线程安全(<clinit>() 方法仅执行一次),无需额外加锁;最终实现仅当调用 getInstance() 时才初始化单例实例的延迟加载,同时规避非必要实例化的资源浪费。

优化实现代码:

java 复制代码
public class Singleton {
    // 私有构造函数
    private Singleton() {
    }

    // 静态内部类
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
相关推荐
断春风2 小时前
从 JDK 8 到 JDK 21:企业级 Java 版本选择的架构思考
java·架构·jdk
h7ml2 小时前
构建可扩展的企业微信消息推送服务:事件驱动架构在Java中的应用*
java·架构·企业微信
heartbeat..2 小时前
JavaWeb 核心:HttpServletRequest 请求行、请求头、请求参数完整梳理
java·网络·web·request
Aliex_git2 小时前
内存堆栈分析笔记
开发语言·javascript·笔记
LYOBOYI1233 小时前
qml练习:创建地图玩家并且实现人物移动(2)
开发语言·qt
电商API&Tina3 小时前
【电商API接口】多电商平台数据API接入方案(附带实例)
运维·开发语言·数据库·chrome·爬虫·python·jenkins
叫我莫言鸭3 小时前
关于word生成报告的POI学习2循环标题内容
java·学习·word
1001101_QIA3 小时前
【C++笔试题】递归判断数组是否是递增数组
开发语言·c++
zhangx1234_3 小时前
C语言 题目2
c语言·开发语言