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》推荐,防止反射、序列化破坏
相关推荐
ClinicTech2 小时前
实验室洗瓶机的清洗系统架构与自动化控制解析
java·python
linmengmeng_13142 小时前
【总结】MyBatis-Plus批量插入与清表的正确姿势
java·spring boot·mysql·mybatis
木井巳2 小时前
【记忆化搜索】不同路径
java·算法·leetcode·深度优先·剪枝·推荐算法
Sam_Deep_Thinking2 小时前
new Thread()之后发生了什么?
java·后端·面试·程序员
泡海椒2 小时前
规则引擎对比:JQuick-Java vs Drools 轻量场景选型对比
java·开发语言
wno7042 小时前
Spring Security短信验证码登录
java·python·spring
步行cgn2 小时前
Spring util 命名空间详解
java·后端·spring
古法安卓3 小时前
Android-休眠唤醒后onLocationChanged没有数据问题排查
android·java·android studio
周GZ3 小时前
简单讲解线程池的4种拒绝策略
java