跳过正文
  1. 博客/

单例模式的几种写法

·273 字·2 分钟

单例模式的几种写法,你用哪种?
#

为什么需要单例
#

单例模式大概是最简单也最常被问到的设计模式了。面试必问,但你真的理解每种写法的区别吗?

先说为什么需要单例。有些对象全局只需要一个就够了,比如线程池、数据库连接池、配置管理器。你不希望到处 new,浪费资源不说,状态不一致更头疼。

单例的核心就两件事:构造方法私有化,提供一个全局访问点。听起来简单,但写法还挺多的,逐个来看。

饿汉式
#

最简单直接的写法:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

类加载的时候就创建实例了,所以叫"饿汉"——还没等你要,它就准备好了。

优点:简单,线程安全(JVM 类加载机制保证的)。 缺点:不管用不用都会创建,如果对象很重,就有点浪费。

说实话,大部分场景用饿汉式就够了。你的单例对象真的很重吗?多数时候并不是。

懒汉式
#

既然饿汉式可能浪费,那就等需要的时候再创建:

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这就是懒汉式——用的时候才创建。

但问题来了:多线程环境下不安全。两个线程同时判断 instance == null,都通过了,就会创建两个实例。

加个 synchronized 能解决:

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

但这样每次调用 getInstance 都要加锁,性能太差了。实例都创建好了还加锁,完全没必要。

双重检查锁(DCL)
#

为了解决懒汉式的性能问题,有了双重检查锁:

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,不能少。为什么?因为 new Singleton() 实际分三步:分配内存、初始化对象、赋值引用。JVM 可能重排序,导致另一个线程拿到还没初始化完的对象。volatile 禁止重排序,保证安全。

说到这里,DCL 面试出现频率超高。把为什么要两次检查、为什么要 volatile 讲清楚,面试官基本满意了。

静态内部类
#

public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

利用了 JVM 的类加载机制:内部类 Holder 在第一次被引用时才会加载,加载的时候创建实例。既实现了延迟加载,又保证了线程安全。

我个人挺喜欢这种写法的,简洁优雅,不用操心并发问题。

枚举实现
#

《Effective Java》里推荐的写法:

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务逻辑
    }
}

就这么几行。枚举天生就是单例,JVM 保证的。而且枚举还能防止反射攻击和反序列化破坏单例——前面几种写法都做不到这点。

你可能觉得这写法看着不像正常的类。确实,枚举做单例在国内项目里不太常见。但从安全性角度来说,它确实是最完美的。

到底用哪个
#

整理一下对比:

写法延迟加载线程安全防反射防反序列化
饿汉式
懒汉式+sync
DCL
静态内部类
枚举

我的建议:

  • 一般场景用饿汉式,简单不出错
  • 需要延迟加载用静态内部类
  • 面试重点讲DCL,把 volatile 那块讲明白
  • 追求完美用枚举

其实吧,在 Spring 项目里,大部分单例需求直接用 Spring 的单例 Bean 就行了,你手写单例的机会真不多。但面试嘛,该会还是得会。

我之前面试被追问"DCL 不加 volatile 会怎样",当时答得磕磕绊绊的。后来花时间研究了 JMM,才真正搞明白。并发相关的东西,光背八股不够,得理解底层原理才行。