单例模式:饿汉式、懒汉式、双重检查锁、静态内部类、枚举实现、实战:配置管理器

单例模式,说白了就是一个类在JVM里只能有一个实例。你可能会问:为什么非要限制实例个数?我在项目中遇到过好几次,比如日志记录器、线程池、缓存管理器,这些对象如果被反复创建,要么浪费内存,要么导致状态不一致。嗯,单例模式就是用来解决这类问题的。

饿汉式:简单粗暴,但有点浪费

饿汉式,顾名思义,类加载的时候就急着把实例创建好。代码写起来很直接:

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

我个人习惯在确定这个类一定会被用到时才用饿汉式。如果你写了一个工具类,但项目启动后可能根本没人调用它,那这个实例就白白占着内存了。我曾经在一个微服务项目里看到有人把所有工具类都写成饿汉式,结果内存占用直接飙高——其实很多类根本没用上。

适用场景:实例较小、一定会被使用、初始化逻辑简单。

懒汉式:需要时才创建,但线程不安全

懒汉式就聪明一点,等到第一次调用 getInstance() 时才创建实例。基础版本长这样:

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

但这里有个坑——多线程环境下,两个线程可能同时进入 if (instance == null),然后各自创建了一个实例。这就违背了单例的初衷。我刚开始写Java时就在这个坑里摔过,线上出现了两个不同的配置管理器实例,导致配置读取结果不一致,排查了半天。

注意:懒汉式基础版在多线程下不安全,需要加锁处理。

双重检查锁:兼顾性能和线程安全

为了解决懒汉式的线程安全问题,同时避免每次调用都加锁影响性能,双重检查锁(DCL)就登场了:

public class DCLSingleton {
    private static volatile DCLSingleton instance;
    
    private DCLSingleton() {}
    
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

这里有两个关键点。第一,volatile 关键字是必须的,它防止指令重排序。你想想看,new DCLSingleton() 这个操作在底层不是原子的——先分配内存、再初始化对象、最后把引用指向内存。如果不加 volatile,线程可能拿到一个半初始化的对象。第二,双重检查保证了只有第一次创建时才进入同步块,后续调用直接返回实例,性能几乎无损。

我曾经在一个高并发的配置中心项目里用的就是 DCL,运行了两年多没出过问题。不过说实话,DCL 的代码可读性一般,团队里新人容易看不懂。

静态内部类:优雅且高效

我个人最喜欢的方式是静态内部类实现。它利用了JVM的类加载机制,既实现了懒加载,又保证了线程安全:

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

原理其实很简单:外部类加载时,内部类 Holder 并不会被加载。只有当你调用 getInstance() 时,JVM 才会去加载 Holder,同时创建实例。而JVM保证类的加载过程是线程安全的,所以不需要额外的同步代码。

嗯,这里要注意:静态内部类方式不能处理序列化问题。如果你的单例类需要实现 Serializable,记得加上 readResolve() 方法,否则反序列化时会创建新实例。

枚举实现:最简洁,最安全

Joshua Bloch 在《Effective Java》里大力推荐枚举实现。代码简单到令人发指:

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

枚举实现天然防止反射攻击和序列化问题。你想想看,反射调用私有构造器在枚举面前是无效的,反序列化也不会创建新实例。我在一个安全要求很高的金融项目里就用了枚举单例,省去了很多防御性代码。

不过枚举的缺点也很明显——它不能继承其他类(枚举已经继承了 Enum),而且如果单例需要懒加载,枚举不太合适,因为枚举实例在类加载时就被创建了。

六种实现对比

实现方式 线程安全 懒加载 防反射 防序列化 推荐指数
饿汉式 ★★★
懒汉式(基础)
懒汉式(同步) ★★
双重检查锁 ★★★★
静态内部类 ★★★★★
枚举 ★★★★★

实战:配置管理器

说了这么多理论,咱们来点实际的。配置管理器是企业应用里最常见的单例场景之一。我参与过的一个电商系统,配置信息分散在数据库、Redis、本地文件里,需要一个统一的入口来管理。

下面是一个基于静态内部类的配置管理器实现:

public class ConfigurationManager {
    
    private Properties properties = new Properties();
    
    private ConfigurationManager() {
        loadConfig();
    }
    
    private static class Holder {
        private static final ConfigurationManager INSTANCE = new ConfigurationManager();
    }
    
    public static ConfigurationManager getInstance() {
        return Holder.INSTANCE;
    }
    
    private void loadConfig() {
        try (InputStream input = getClass().getClassLoader()
                .getResourceAsStream("application.properties")) {
            properties.load(input);
        } catch (IOException e) {
            // 实际项目中应该用日志框架
            System.err.println("加载配置文件失败: " + e.getMessage());
        }
    }
    
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
    
    public String getProperty(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }
    
    public int getIntProperty(String key, int defaultValue) {
        String value = properties.getProperty(key);
        if (value == null) return defaultValue;
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }
}

使用起来也很简单:

String dbUrl = ConfigurationManager.getInstance().getProperty("db.url");
int maxConnections = ConfigurationManager.getInstance()
        .getIntProperty("db.maxConnections", 10);

我曾经在一个项目里看到有人每次需要配置时都 new 一个 Properties 对象去读文件,结果高并发下文件被反复读取,性能很差。用单例模式管理配置,一次加载,全局共享,既节省资源又保证一致性。

核心要点:
  • 单例模式的核心是私有构造器 + 全局访问点
  • 静态内部类是我个人最推荐的实现方式,兼顾简洁、懒加载、线程安全
  • 枚举实现最安全,适合对安全性要求极高的场景
  • 双重检查锁适合需要懒加载且对性能敏感的场景
  • 配置管理器是单例模式的经典应用,注意加上缓存刷新机制

最后说一句,单例模式虽然好用,但别滥用。如果一个类不需要全局唯一性,强行写成单例反而会增加耦合,让单元测试变得困难。我见过有人把每个 Service 都写成单例,结果测试时 mock 都费劲。嗯,设计模式是工具,不是教条。

单例模式实现方式对比 饿汉式 类加载时创建 懒汉式 首次调用时创建 双重检查锁 volatile + 同步 静态内部类 JVM类加载机制保证线程安全 枚举实现 最安全,防反射防序列化 实战:配置管理器 统一管理配置,一次加载全局共享 推荐:静态内部类(通用) | 枚举(安全场景) | 双重检查锁(高性能懒加载)

公众号:蓝海资料掘金营,微信deep3321