懒汉式和饿汉式区别:一文搞懂单例模式的两种实现方式

懒汉式和饿汉式区别:从加载时机说起

在写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关键字,防止指令重排序,确保其他线程看到的是完整的对象。这种写法在实际开发中很常见,兼顾了效率和安全。

实际场景怎么选?

如果你的对象初始化成本不高,或者程序启动后大概率会用到,那直接用饿汉式更省心,代码简单还安全。比如项目里的全局配置类,一启动就要读取,这时候饿汉式正合适。

但如果这个对象占内存大,创建耗时长,而且用户操作不一定触发使用,那就用懒汉式,避免资源浪费。比如某个报表生成器,普通用户一年用不上几次,就没必要一上来就加载。

说白了,饿汉式是“宁可错杀,不可放过”,懒汉式是“不见兔子不撒鹰”。选哪个,得看具体场景。