Spring + 设计模式 (三) 创建型 - 单例模式

192 阅读8分钟

单例模式

引言

单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。其核心在于控制实例化过程,避免重复创建对象,节约资源并保证行为一致性。单例模式适用于需要全局唯一对象的场景,如配置管理、线程池或日志系统。它简洁却威力巨大,是设计模式中最常用也最易被误用的模式之一。

实际开发中的用途

在实际开发中,单例模式常用于管理共享资源或全局状态。例如,数据库连接池通常设计为单例,避免频繁创建连接带来的性能损耗;日志系统也常采用单例,确保所有模块写入同一日志文件。单例模式解决了多实例导致的资源竞争或状态不一致问题,但需注意线程安全和测试难度,避免滥用。

开发中的示例

设想一个系统配置管理器,需要在整个应用中共享同一份配置数据。通过单例模式,可以确保所有模块访问的是同一配置实例,避免多次读取配置文件或配置不一致的问题。例如,Spring Boot的Environment对象本质上就是一个单例,统一管理应用配置。

单例模式有多种实现方式,每种方式都有其特点和适用场景。以下是单例模式的全部实现方式及其详细说明:

1. 饿汉式(Eager Initialization)

特点:类加载时就初始化实例,线程安全但可能造成资源浪费

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

优点

  • 实现简单
  • 线程安全(由JVM类加载机制保证)

缺点

  • 即使不使用也会创建实例,可能浪费内存
  • 不能延迟加载

2. 懒汉式(Lazy Initialization)

基础版(非线程安全)
public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

问题:多线程环境下可能创建多个实例

同步方法版(线程安全但性能低)
public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;
    
    private SynchronizedSingleton() {}
    
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

优点:线程安全 缺点:每次获取实例都要同步,性能差

双重检查锁(DCL,推荐)
public class DCLSingleton {
    private volatile static DCLSingleton instance;
    
    private DCLSingleton() {}
    
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

特点

  • 线程安全
  • 延迟加载
  • 高性能(只有第一次创建需要同步)

注意:必须使用volatile关键字防止指令重排序

3. 静态内部类(Holder模式,推荐)

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

优点

  • 线程安全(由JVM类加载机制保证)
  • 延迟加载(只有调用getInstance()时才加载内部类)
  • 实现简单

4. 枚举单例(Effective Java推荐)

public enum EnumSingleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务方法
    }
}

优点

  • 线程安全
  • 防止反射攻击
  • 防止反序列化重新创建对象
  • 实现简单

缺点

  • 不能延迟加载
  • 不够灵活(不能继承其他类)

5. ThreadLocal单例(线程内单例)

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> instance = 
        ThreadLocal.withInitial(ThreadLocalSingleton::new);
    
    private ThreadLocalSingleton() {}
    
    public static ThreadLocalSingleton getInstance() {
        return instance.get();
    }
}

特点

  • 每个线程有自己唯一的实例
  • 适用于需要线程隔离的场景

6. Spring容器管理的单例(最实用)

@Component
@Scope("singleton") // 默认就是singleton,可省略
public class SpringSingleton {
    // Spring会管理这个单例的生命周期
}

特点

  • 由IoC容器管理生命周期
  • 支持依赖注入
  • 线程安全
  • 支持各种初始化/销毁回调
  • 最推荐的生产环境使用方式

各实现方式对比

实现方式线程安全延迟加载防止反射防止反序列化性能复杂度
饿汉式
懒汉式(同步方法)
双重检查锁
静态内部类
枚举
Spring容器管理

最佳实践建议

在实际开发中,选择单例模式的实现方式需要根据具体场景和技术栈做出合理决策。

Java SE 环境下,枚举方式是最佳选择,它不仅线程安全,还能防止反射和反序列化破坏单例,代码简洁明了。如果业务场景需要延迟加载,则可以采用静态内部类(Holder模式),它在保证线程安全的同时,实现了按需加载,兼顾性能和内存效率。

对于 Java EE 或 Spring 环境,应当优先使用 Spring 容器管理的单例。Spring 的 IoC 容器天然支持单例模式,不仅自动管理生命周期,还提供依赖注入、AOP 增强等高级特性,避免了手动实现单例的复杂性,同时确保线程安全和可测试性。

在某些特殊场景下,单例的选择需要更加灵活。例如,在需要线程隔离的情况下(如 Web 请求上下文管理),ThreadLocal 单例可以确保每个线程拥有独立的实例,避免并发问题。而在极端性能敏感的场景(如高频调用的工具类),如果不需要延迟加载,饿汉式单例由于没有同步开销,可能成为最优解。

然而,单例模式并非万能钥匙,在使用时需注意避免以下情况:

  • 避免手动实现复杂的单例逻辑(如双重检查锁),除非有充分理由,否则优先使用更简洁的方式(如枚举或 Spring 管理)。
  • 避免非线程安全的实现,尤其是在多线程环境下,懒汉式基础版会导致难以调试的并发问题。
  • 避免过度使用单例,全局状态的管理会增加代码的耦合度,使得单元测试和维护变得困难。单例应当仅用于真正的全局唯一资源(如配置管理、线程池),而非滥用为"方便访问"的工具。

合理选择单例实现方式,结合框架能力,才能在保证功能正确性的同时,兼顾代码的可维护性和性能。

Spring 源码中的应用

Spring框架中,单例模式是Bean作用域的默认行为。在DefaultSingletonBeanRegistry类中,Spring通过singletonObjects缓存实现单例管理。以下是Spring源码的典型片段(DefaultSingletonBeanRegistry.java):

// Spring框架中的单例注册实现
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    @Override
    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                singletonObject = singletonFactory.getObject();
                addSingleton(beanName, singletonObject);
            }
            return singletonObject;
        }
    }

    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.put(beanName, singletonObject);
            // 其他相关缓存清理...
        }
    }
}

这段代码展示了Spring如何通过双重检查锁和ConcurrentHashMap确保单例线程安全。singletonObjects缓存存储所有单例Bean,getSingleton方法通过同步块保证原子性操作。Spring的单例实现不仅解决了线程安全问题,还支持依赖注入和生命周期管理,比传统单例模式更加强大和灵活。

SpringBoot 代码案例

以下是一个基于Spring Boot的单例模式案例,实现一个线程安全的配置管理器:

// 配置管理器接口
public interface ConfigManager {
    String getConfig(String key);
    void setConfig(String key, String value);
}

// 单例实现
@Component
@Scope("singleton")
public class SingletonConfigManager implements ConfigManager {
    private final Map<String, String> configMap = new HashMap<>();

    @PostConstruct
    public void init() {
        // 初始化加载默认配置
        configMap.put("app.name", "SingletonDemo");
        configMap.put("app.version", "1.0.0");
    }

    @Override
    public synchronized String getConfig(String key) {
        return configMap.get(key);
    }

    @Override
    public synchronized void setConfig(String key, String value) {
        configMap.put(key, value);
    }
}

// 测试控制器
@RestController
public class ConfigController {
    @Autowired
    private ConfigManager configManager;

    @GetMapping("/config")
    public String getConfig(@RequestParam String key) {
        return configManager.getConfig(key);
    }

    @GetMapping("/updateConfig")
    public String updateConfig(@RequestParam String key, @RequestParam String value) {
        configManager.setConfig(key, value);
        return "Config updated";
    }
}

这个案例展示了Spring Boot中单例模式的典型应用。SingletonConfigManager通过@Component@Scope("singleton")声明为单例Bean,Spring容器保证全局唯一实例。控制器通过依赖注入获取配置管理器,所有请求共享同一配置状态。Spring的单例管理不仅线程安全,还支持完整的生命周期控制(如@PostConstruct初始化),比手动实现单例更加可靠和灵活。

总结

单例模式是设计模式中的"瑞士军刀",简洁却功能强大。Spring将其发挥到极致,通过IoC容器管理单例生命周期,解决了传统实现中的线程安全和测试难题。但切记:单例不是万能药,滥用会导致代码难以测试和维护。在Spring生态中,应优先使用容器管理的单例Bean,而非手动实现。掌握单例模式的精髓,才能在保证性能的同时,写出优雅可维护的代码。

(对您有帮助 && 觉得我总结的还行) -> 受累点个免费的赞👍,谢谢