懒汉式和饿汉式区别:从加载时机说起
在写Java程序的时候,单例模式用得不少。比如配置管理、数据库连接池、日志对象这些,通常只需要一个实例就够了。而说到单例,绕不开两个词:懒汉式和饿汉式。它们最根本的区别,就在“什么时候创建对象”。
饿汉式:一上来就创建
顾名思义,饿汉式就像个急性子,类一加载,就把实例给new出来了,不管你后面用不用得上。这种方式简单粗暴,但能保证线程安全。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
你看,instance是static final的,在类加载阶段就完成了初始化。没有判断,没有延迟,直接用。好处是执行快,调用getInstance()的时候不花时间;坏处是可能浪费内存——万一这个对象一直没用呢?
懒汉式:用的时候才创建
懒汉式就比较佛系了,它不急着创建对象,等到第一次调用getInstance()的时候才动手。这叫延迟加载,适合那些启动慢、占用资源多、不一定用得上的对象。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
看起来挺好,但问题来了:多线程环境下,两个线程同时发现instance是null,就会各自创建一个实例,单例就失效了。所以这种写法在并发场景下是不安全的。
加锁解决线程安全问题
为了保证懒汉式在多线程下也只创建一次,最常见的做法就是加同步锁:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
加了synchronized之后,确实线程安全了,但每次调用都要排队,性能受影响。其实只有第一次创建需要同步,后面完全可以直接返回,没必要每次都锁。
双重检查锁定优化懒汉式
于是就有了双重检查(Double-Checked Locking)写法,既保证性能又保证安全:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里用了volatile关键字,防止指令重排序,确保其他线程看到的是完整的对象。这种写法在实际开发中很常见,兼顾了效率和安全。
实际场景怎么选?
如果你的对象初始化成本不高,或者程序启动后大概率会用到,那直接用饿汉式更省心,代码简单还安全。比如项目里的全局配置类,一启动就要读取,这时候饿汉式正合适。
但如果这个对象占内存大,创建耗时长,而且用户操作不一定触发使用,那就用懒汉式,避免资源浪费。比如某个报表生成器,普通用户一年用不上几次,就没必要一上来就加载。
说白了,饿汉式是“宁可错杀,不可放过”,懒汉式是“不见兔子不撒鹰”。选哪个,得看具体场景。