一、什么是单例模式
单例(Singleton)属于创建型设计模式 。 核心目标:保证一个类在整个程序运行期间,全局只有唯一的一个实例对象,并且对外提供获取该实例的统一入口。
适用场景:
- 数据库连接池、线程池、配置文件管理器、日志工具类
- 这类对象不需要多次创建,频繁new会浪费内存、资源。
单例的两个核心要点:
- 私有化构造方法,禁止外部直接new对象
- 提供静态方法,对外返回唯一实例
二、写法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的作用
- 外层if:实例已经创建,直接返回,避免每次都进入同步块抢锁,提升性能
- 内层if:多个线程都通过外层if,在锁外等待;拿到锁之后再次判断,防止重复new对象
重点:为什么必须加volatile?
instance = new SingletonDCL()这一行代码,底层会拆成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,才会加载内部类,初始化实例。
优点:
- 懒加载
- 利用类加载机制,天然线程安全,不需要加锁、不需要volatile
- 代码简洁,推荐使用
**缺点:**无法传递参数创建实例
七、写法6:枚举单例(Effective Java推荐,防反射、防序列化)
public enum SingletonEnum {
INSTANCE;
//业务方法
public void doSomething(){
}
}
优点:
- 写法最简单,天然线程安防止反射破坏单例,防止序列化反序列化生成新对象,是最安全的单例
**缺点:**不支持懒加载
八、单例的破坏方式(拓展)
- 反射:通过反射获取私有构造器,强行newInstance创建新对象,破坏单例。枚举天然抵御反射攻击。
- 序列化与反序列化:普通单例类序列化后,反序列化会生成全新对象;枚举不会。
九、各种单例写法对比表
| 写法 | 懒加载 | 线程安全 | 锁 | 防反射 | 性能 |
|---|---|---|---|---|---|
| 饿汉式 | ❌ | ✅ | 无 | ❌ | 高 |
| 基础懒汉 | ✅ | ❌ | 无 | ❌ | 高 |
| synchronized方法懒汉 | ✅ | ✅ | 方法锁 | ❌ | 低 |
| DCL双重检查锁 | ✅ | ✅ | 同步块+volatile | ❌ | 较高 |
| 静态内部类 | ✅ | ✅ | 无 | ❌ | 高 |
| 枚举单例 | ❌ | ✅ | 无 | ✅ | 高 |
十、总结
- 单例核心:私有构造器 + 全局唯一实例 + 静态获取入口
- 饿汉简单但无懒加载;基础懒汉多线程不安全;加锁方法性能差
- 双重检查锁是重点,volatile禁止指令重排
- 静态内部类:代码简洁,推荐业务开发使用
- 枚举单例:安全性最高,《Effective Java》推荐,防止反射、序列化破坏