一、定义
单例模式:确保一个类只有一个实例,并提供一个全局访问点。
来看看下面的类图:

单例模式有三种不同的实现,应对不同的场景
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. 将单例服务化
这是一种更彻底,更符合微服务架构思想的方案。
- 实现原理
- 将需要单例的功能(例如ID生成、全局配置)独立出来,部署成一个单独的服务。
- 这个服务本身可以是一个集群,但其内部状态是统一的(例如:ID生成器使用数据库序列号或者Redis原子操作)。
- 其他所有服务都通过RPC或http来调用这个单一的服务,从而在逻辑上保证了单例效果。