Java单例模式|从基础到多线程安全

一、什么是单例模式

单例(Singleton)属于创建型设计模式 。 核心目标:保证一个类在整个程序运行期间,全局只有唯一的一个实例对象,并且对外提供获取该实例的统一入口。

适用场景:

  • 数据库连接池、线程池、配置文件管理器、日志工具类
  • 这类对象不需要多次创建,频繁new会浪费内存、资源。

单例的两个核心要点:

  1. 私有化构造方法,禁止外部直接new对象
  2. 提供静态方法,对外返回唯一实例

二、写法1:饿汉式(静态常量,线程安全)

java 复制代码
public class SingletonHungry {
    // 类加载时直接初始化实例
    private static final SingletonHungry instance = new SingletonHungry();
    // 私有构造器,防止外部new
    private SingletonHungry(){}
    // 对外获取实例
    public static SingletonHungry getInstance(){
        return instance;
    }
}

优点: 类加载阶段直接创建实例,天然线程安全,实现简单

**缺点:**类加载就创建对象;即使程序全程不用这个实例,对象也会被创建,占用内存。

三、写法2:懒汉式(基础版,线程不安全)

java 复制代码
public class SingletonLazy {
    private static SingletonLazy instance;
    private SingletonLazy(){}
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

优点: 只有调用getInstance才创建对象,节省内存。

缺陷:多线程环境下线程不安全。

问题1:多个线程同时进入if(instance==null),多个线程同时执行new,会创建多个实例,破坏单例。

问题2:instance这个属性被static修饰全局只有一个 ,随着第二个线程的覆盖 操作,第一个线程new出来的对象也就会被GC给释放掉 ,但,new的过程中会浪费内存和资源,可能要把100G的数据从硬盘加载到内存呀

四、写法3:懒汉式加synchronized方法(低效)

java 复制代码
public class SingletonLazy {
    private static SingletonLazy instance;
    private SingletonLazy(){}
    public static SingletonLazy getInstance(){
        synchronized (locker) {
            //if判定+赋值这两个合在一起,可能涉及到多线程的修改
            if (instance == null) {
                //赋值是原子级操作
                instance = new SingletonLazy();
            }
        }        
        return instance;
    }
}
java 复制代码
public class SingletonLazy {
    private static SingletonLazy instance;
    private SingletonLazy(){}
    // 方法上加锁
    public synchronized static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

优点:解决了线程安全,不会创建多个对象

缺点:锁粒度太大,性能差。每次调用getInstance都要竞争锁,哪怕实例已经创建完成,并发访问时大量线程阻塞。

五、写法4:懒汉式 双重检查锁

java 复制代码
public class SingletonLazy {
    // volatile 不能省略!
    private static volatile SingletonLazy instance;
    private SingletonLazy(){}
    public static SingletonLazy getInstance(){
        // 第一次检查:实例不为空,直接返回,不用抢锁
        if(instance == null){
            //这个写法相当于锁对象换成了类对象 SingletonLazy.class 和上面的方法没区别
            synchronized (SingletonLazy.class){
                // 第二次检查:防止多个线程等待锁之后重复创建
                if(instance == null){
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

两个if的作用

  1. 外层if:实例已经创建,直接返回,避免每次都进入同步块抢锁,提升性能
  2. 内层if:多个线程都通过外层if,在锁外等待;拿到锁之后再次判断,防止重复new对象

重点:为什么必须加volatile?

instance = new SingletonDCL()这一行代码,底层会拆成3步:

  1. 分配对象内存空间
  2. 初始化对象(执行构造方法,给成员变量赋值)
  3. 将instance引用指向分配的内存地址

JVM会发生指令重排 :2、3步骤顺序可能颠倒。 也就是:先分配内存,引用赋值,对象还没初始化完成 。 此时其他线程读取instance,发现不为null,直接返回一个半初始化的对象,出现BUG。

volatile关键字作用:禁止指令重排序,保证上面三步按顺序执行,解决懒汉式的线程安全漏洞。

小结:懒汉式双重检查锁,两层if减少锁竞争,volatile防止new对象时指令重排,保证多线程安全,兼顾懒加载与性能,是项目最常用写法。

六、写法5:静态内部类(推荐,简洁安全)

java 复制代码
public class SingletonInner {
    private SingletonInner(){}
    //静态内部类
    private static class SingletonHolder{
        private static final SingletonInner instance = new SingletonInner();
    }
    public static SingletonInner getInstance(){
        return SingletonHolder.instance;
    }
}

原理 :外部类加载时,静态内部类不会加载;只有调用getInstance,才会加载内部类,初始化实例。

优点:

  1. 懒加载
  2. 利用类加载机制,天然线程安全,不需要加锁、不需要volatile
  3. 代码简洁,推荐使用

**缺点:**无法传递参数创建实例

七、写法6:枚举单例(Effective Java推荐,防反射、防序列化)

复制代码
public enum SingletonEnum {
    INSTANCE;
    //业务方法
    public void doSomething(){
    }
}

优点:

  • 写法最简单,天然线程安防止反射破坏单例,防止序列化反序列化生成新对象,是最安全的单例

**缺点:**不支持懒加载

八、单例的破坏方式(拓展)

  1. 反射:通过反射获取私有构造器,强行newInstance创建新对象,破坏单例。枚举天然抵御反射攻击。
  2. 序列化与反序列化:普通单例类序列化后,反序列化会生成全新对象;枚举不会。

九、各种单例写法对比表

写法 懒加载 线程安全 锁 防反射 性能
饿汉式 ❌ ✅ 无 ❌ 高
基础懒汉 ✅ ❌ 无 ❌ 高
synchronized方法懒汉 ✅ ✅ 方法锁 ❌ 低
DCL双重检查锁 ✅ ✅ 同步块+volatile ❌ 较高
静态内部类 ✅ ✅ 无 ❌ 高
枚举单例 ❌ ✅ 无 ✅ 高

十、总结

  1. 单例核心:私有构造器 + 全局唯一实例 + 静态获取入口
  2. 饿汉简单但无懒加载;基础懒汉多线程不安全;加锁方法性能差
  3. 双重检查锁是重点,volatile禁止指令重排
  4. 静态内部类:代码简洁,推荐业务开发使用
  5. 枚举单例:安全性最高,《Effective Java》推荐,防止反射、序列化破坏
相关推荐
Sarvartha2 小时前
final 关键字
java·开发语言
2601_952047792 小时前
单一策略深度教程:用轻易云把集成任务的报错实时推送到钉钉机器人
java·机器人·钉钉
数据狐(Datafox)3 小时前
淘宝图片搜索 API 落地实战:基于以图搜货搭建跨境电商选品系统
java·大数据·微服务
Escalating_xu5 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
灯澜忆梦5 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
一个风轻云淡5 小时前
GCC 和 GDB命令简单解读
java·linux·前端
省钱兄--zs5 小时前
西安24小时自助健身房解决方案实战:系统开发与部署全流程指南
java·spring boot·系统架构·intellij-idea·需求分析
资料库016 小时前
Linux 8个常见运维故障排查
java·linux·运维