Head First 单例模式

一、定义

单例模式:确保一个类只有一个实例,并提供一个全局访问点。

来看看下面的类图:

单例模式有三种不同的实现,应对不同的场景

1、懒汉式(也叫延迟实例化)

为什么叫懒汉式?

因为类实例化的代码是在运行时真正用到的时候才执行的。就好像人不会提前准备好,等用到的时候才着手干。

csharp 复制代码
public class LazySingleton {

    /**
     * 静态的变量,持有LazySingleton的实例
     */
    private static LazySingleton instance;

    /**
     * 私有构造方法
     */
    private LazySingleton() {}

    /**
     * 这个方法是线程不安全的
     * @return
     */
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

在上述代码中,实现了懒汉式的单例模式。但是它不是线程安全的。当在多线程环境下运行时,可能导致会有多个实例对象。

下面就要将上述代码改造成线程安全的。

第一种方法:使用同步锁synchronized

通过增加syncronized关键字到getInstance方法中,迫使每个线程在进入这个方法之前,要先等候别的线程离开该方法。

csharp 复制代码
public class SafeLazySingleton {

    /**
     * 静态的变量,持有SafeLazySingleton的实例
     */
    private static SafeLazySingleton instance;

    /**
     * 私有构造方法
     */
    private SafeLazySingleton() {}

    /**
     * 这个方法通过使用同步锁synchronized确保是线程安全的,但是会导致性能降低
     * @return
     */
    public static synchronized SafeLazySingleton getInstance() {
        if (instance == null) {
            instance = new SafeLazySingleton();
        }
        return instance;
    }
}

第二种方法:在JVM初次加载类时就完成实例化

依赖JVM在加载这个类时马上创建唯一的单件实例。JVM保证在任何线程访问instance静态变量之前,一定先创建此实例。

2、饿汉式

为什么叫饿汉式?

不是在需要时才创建实例,而是在JVM加载类时马上创建实例,表现出非常急切的状态。就像人饿久了就会非常急切的想吃饭一样。

csharp 复制代码
public class HungrySingleton {
    // 在静态初始化器中创建单件。这行代码保证了线程安全
    private static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return instance;
    }
}

第三种方法:使用双重检查加锁

利用双重检查加锁,首先检查是否实例已经创建了,如果尚未创建,才进行同步。这样一来,只会第一次同步。

3、双重检查加锁

指的是:volatile关键字和同步锁synchronized

volatile关键字确保当instance变量被初始化成Singleton实例时,多个线程正确的处理instance变量。 synchronized关键字确保多线程同步执行。

csharp 复制代码
public class DoubleCheckLockSingleton {

    // 使用volatile关键字确保instance是线程安全的
    private static volatile DoubleCheckLockSingleton instance;
    private DoubleCheckLockSingleton() {}

    public static DoubleCheckLockSingleton getInstance() {
        if (instance == null) {
            // 只有在instance是null的时候才会进入使用同步锁
            synchronized (DoubleCheckLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }
}
线程安全方式 优点 缺点 使用场景
synchronized关键字 简单又有效的实现线程安全 严重降低性能。其实只有第一次调用getInstance方法时才真正需要同步。一旦设置好instance变量,就不需要同步了。但是现状是每次调用都会同步 应用程序可以接受synchronized带来的额外负担,并且genInstance方法并不会被频繁调用
饿汉式单例 线程安全 过早的创建实例,如果实例一直没有被用到或者实例占用资源很大,容易造成资源浪费 1. 应用程序总是创建并使用单件实例。2. 应用程序在创建和运行时方面的负担不太繁重,想要急切的创建单件实例。
双重检查加锁 1. 线程安全。2. 只有第一次访问时会使用syncronized锁,所以并不会降低性能。 代码实现比前两种复杂 任何需要单例类的场景中

二、使用场景

1、SpringBoot中的单例模式

在SpringBoot中,单例模式是依赖注入容器默认管理Bean的方式,通过@Service、@Component等注解标记的类会被自动注册为单例Bean,确保在整个应用上下文中仅存在一个实例。
实现方式与默认行为:SpringBoot使用IoC容器管理Bean的生命周期,默认作用域为单例。例如:使用@Service注解的类:

typescript 复制代码
@Service
public class GreetingServiceImpl implements GreetingService {
    @Override
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

通过@Autowired注入时,无论在多少个组件中使用,获取的都是同一个实例。
线程安全与最佳实践 :单例Bean必须是无状态的 ,避免在实例中保存用户相关数据,否则可能引发线程安全问题。正确做法是将状态存储在方法参数或者局部变量中,而非成员变量。
自定义作用域与延迟加载 :虽然默认为单例,但可通过@Scope("prototype")显示指定为多例模式(每次获取新实例)。对于单例Bean,可通过使用@Lazy注解实现懒加载,延迟初始化直到首次注入时才创建实例。
与传统单例模式的区别:Spring的单例由容器管理,无需手动编写getInstance方法或同步锁,简化了代码并避免了线程安全问题,同时提供了更灵活的配置选项。

三、分布式架构中如何使用单例模式?如何确保全局只有一个实例?

在分布式系统中,单例模式的核心目标是确保某个类在整个系统中只有一个实例。例如,你可能需要一个全局的ID生成器、配置管理中心或任务调度器。但分布式环境的跨节点特性使得传统单例(例如Java JVM内的单例)失效。因为每个节点可以独自加载类并创建实例。因此,需要借助外部协调机制来实现全局唯一性。
分布式单例的核心就是把单例的控制权从单JVM延伸到多JVM。

以下是几种主流的解决方案:

1. 借助外部中间件实现

这是最常用、最专业的做法。思想是让一个外部系统来决定哪个实例是主实例。
1.1 使用Zookeeper/Etcd实现

Zookeeper的临时节点watch机制 非常适合实现分布式锁和Leader选举。
1.2 使用Redis实现 利用Redis的set key value NX PX timeout命令实现分布式锁。

2. 将单例服务化

这是一种更彻底,更符合微服务架构思想的方案。

  • 实现原理
  1. 将需要单例的功能(例如ID生成、全局配置)独立出来,部署成一个单独的服务。
  2. 这个服务本身可以是一个集群,但其内部状态是统一的(例如:ID生成器使用数据库序列号或者Redis原子操作)。
  3. 其他所有服务都通过RPC或http来调用这个单一的服务,从而在逻辑上保证了单例效果。

参考文章:blog.csdn.net/yitiaoxiany...

相关推荐
半夏知半秋1 天前
rust学习-循环
开发语言·笔记·后端·学习·rust
爬山算法1 天前
Hibernate(25)Hibernate的批量操作是什么?
java·后端·hibernate
KawYang1 天前
Spring Boot 使用 PropertiesLauncher + loader.path 实现外部 Jar 扩展启动
spring boot·后端·jar
青梅主码1 天前
2026开年第一炸!陈天桥携代季峰发布 MiroThinker 1.5:30B参数跑出 1T 性能,搜索智能体天花板来了
后端
数据小馒头1 天前
MySQL 性能调优:从EXPLAIN到JSON索引优化
后端
柒.梧.1 天前
深度解析Spring Bean生命周期以及LomBok插件
java·后端·spring
数据小馒头1 天前
告别“For循环”:掌握SQL技巧,让数据处理飞起来
后端
golang学习记1 天前
🌴 Go企业级全栈式框架:Goyave入门和使用介绍
后端
用户908324602731 天前
大模型还在硬编码?Spring AI 实现“动态热切换”全攻略(上)
后端·openai