单例模式学习笔记
📚 目录
- [1. 什么是单例?](#1. 什么是单例? "#1-%E4%BB%80%E4%B9%88%E6%98%AF%E5%8D%95%E4%BE%8B")
- [2. 为什么需要单例?](#2. 为什么需要单例? "#2-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%8D%95%E4%BE%8B")
- [3. 单例的实现方式](#3. 单例的实现方式 "#3-%E5%8D%95%E4%BE%8B%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F")
- [4. RPC框架中的单例应用](#4. RPC框架中的单例应用 "#4-rpc%E6%A1%86%E6%9E%B6%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E5%BA%94%E7%94%A8")
- [5. 单机单例 vs 集群单例](#5. 单机单例 vs 集群单例 "#5-%E5%8D%95%E6%9C%BA%E5%8D%95%E4%BE%8B-vs-%E9%9B%86%E7%BE%A4%E5%8D%95%E4%BE%8B")
- [6. 使用场景判断](#6. 使用场景判断 "#6-%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E5%88%A4%E6%96%AD")
- [7. 最佳实践](#7. 最佳实践 "#7-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
1. 什么是单例?
🎯 简单定义
单例 = 全世界只有一个,不管你在哪里、什么时候拿到的都是同一个东西!
🏠 生活中的类比
- 🌍 地球:全宇宙只有一个地球
- 🏛️ 村委会:村里只能有一个村委会
- 🏪 村里的小卖部:村里只开一家小卖部
- 🔧 村里的工具箱:大家共用一套工具
💻 代码对比
❌ 不是单例(每次都新建)
java
// 每次都创建新的计算器
Calculator calc1 = new Calculator(); // 创建了第1个
Calculator calc2 = new Calculator(); // 创建了第2个
Calculator calc3 = new Calculator(); // 创建了第3个
System.out.println(calc1 == calc2); // false,不是同一个
✅ 单例(全世界只有一个)
java
// 使用单例工厂,不管调用多少次,拿到的都是同一个
Calculator calc1 = SingletonFactory.getInstance(Calculator.class);
Calculator calc2 = SingletonFactory.getInstance(Calculator.class);
Calculator calc3 = SingletonFactory.getInstance(Calculator.class);
System.out.println(calc1 == calc2); // true,是同一个!
System.out.println(calc2 == calc3); // true,还是同一个!
2. 为什么需要单例?
✅ 单例的优势
1. 节省内存资源
java
// 不用单例:创建1000个计算器对象
for (int i = 0; i < 1000; i++) {
Calculator calc = new Calculator(); // 浪费内存!
}
// 用单例:始终只有1个计算器对象
for (int i = 0; i < 1000; i++) {
Calculator calc = SingletonFactory.getInstance(Calculator.class); // 节省内存!
}
2. 保证数据一致性
java
// 服务注册表必须全局唯一
ServiceProvider provider1 = SingletonFactory.getInstance(ServiceProvider.class);
ServiceProvider provider2 = SingletonFactory.getInstance(ServiceProvider.class);
provider1.registerService("用户服务", userService); // 注册服务
Object service = provider2.getService("用户服务"); // 能找到!因为是同一个实例
3. 提升性能
- 🔥 减少对象创建开销
- 🔥 降低GC压力
- 🔥 提高缓存命中率
3. 单例的实现方式
3.1 饿汉式(类加载时创建)
java
public class EagerSingleton {
// 类加载时就创建实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点: 线程安全,实现简单 缺点: 可能造成资源浪费(不用也会创建)
3.2 懒汉式(使用时才创建)
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
// 线程不安全版本
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
优点: 按需创建,节省资源 缺点: 线程不安全
3.3 双重检查锁(推荐)
java
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
// 第一次检查:快速读取(无锁)
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
// 第二次检查:防止重复创建
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
优点: 线程安全 + 性能好 + 按需创建
3.4 单例工厂(RPC框架使用)
java
@Slf4j
public final class SingletonFactory {
private static final Map<String, Holder<Object>> OBJECT_MAP_NEW = new HashMap<>();
private static final Object lock = new Object();
public static <T> T getInstance(Class<T> c) {
String key = c.getName();
// 1. 第一次检查:快速读取缓存(无锁)
Holder<Object> holder = OBJECT_MAP_NEW.get(key);
if (holder != null && holder.get() != null) {
return c.cast(holder.get());
}
// 2. 同步块:确保只有一个线程创建实例
synchronized (lock) {
// 3. 第二次检查:防止其他线程已创建
holder = OBJECT_MAP_NEW.computeIfAbsent(key, k -> new Holder<>());
if (holder.get() == null) {
try {
Constructor<T> constructor = c.getDeclaredConstructor();
constructor.setAccessible(true);
T instance = constructor.newInstance();
holder.set(instance);
} catch (Exception e) {
throw new RuntimeException("创建实例失败", e);
}
}
}
return c.cast(holder.get());
}
}
特点:
- 🎯 通用性:可以为任何类创建单例
- 🔒 线程安全:使用双重检查锁
- 📦 缓存机制:避免重复创建
- 🛡️ 异常处理:创建失败有明确提示
4. RPC框架中的单例应用
4.1 服务提供者(ServiceProvider)- 经典问题场景
🏪 问题场景:村里的服务登记处
想象你们村里有一个"服务登记处",专门记录村里有哪些服务(比如修鞋的、理发的、修电器的)。
❌ 如果用 new
会发生什么?
java
// 如果每个地方都用 new
public class NettyRpcServer {
private final ServiceProvider serviceProvider = new ZkServiceProviderImpl(); // 创建登记处A
}
public class SocketRpcServer {
private final ServiceProvider serviceProvider = new ZkServiceProviderImpl(); // 创建登记处B
}
public class RpcRequestHandler {
private final ServiceProvider serviceProvider = new ZkServiceProviderImpl(); // 创建登记处C
}
结果:村里出现了3个登记处!
🤯 会出现什么问题?
java
// 张三去登记处A登记修鞋服务
NettyRpcServer server = new NettyRpcServer();
server.registerService("修鞋服务", 张三修鞋店); // 登记在A处
// 李四想找修鞋服务,但他去的是登记处B
RpcRequestHandler handler = new RpcRequestHandler();
Object service = handler.getService("修鞋服务"); // 在B处找,找不到!❌
就像这样:
- 📝 登记处A:记录了"修鞋服务"
- 📝 登记处B:空的,什么都没有
- 📝 登记处C:空的,什么都没有
李四去B处找修鞋服务 → 找不到 → 程序报错!
🔍 具体代码对比
❌ 用 new
的问题代码
java
public class NettyRpcServer {
// 每次都创建新的服务提供者
private final ServiceProvider serviceProvider = new ZkServiceProviderImpl();
public void registerService(RpcServiceConfig config) {
serviceProvider.publishService(config); // 服务注册到这个实例
}
}
public class RpcRequestHandler {
// 又创建了一个新的服务提供者
private final ServiceProvider serviceProvider = new ZkServiceProviderImpl();
public Object handle(RpcRequest request) {
// 从这个实例找服务,但服务是注册在上面那个实例里的!
return serviceProvider.getService(request.getRpcServiceName()); // 找不到!❌
}
}
✅ 用单例的正确代码
java
public class NettyRpcServer {
// 获取全局唯一的服务提供者
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
public void registerService(RpcServiceConfig config) {
serviceProvider.publishService(config); // 服务注册到全局实例
}
}
public class RpcRequestHandler {
// 获取同一个全局唯一的服务提供者
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
public Object handle(RpcRequest request) {
// 从同一个全局实例找服务,能找到!
return serviceProvider.getService(request.getRpcServiceName()); // 找到了!✅
}
}
✅ 用单例工厂的好处
java
// 所有地方都用同一个登记处
public class NettyRpcServer {
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
}
public class SocketRpcServer {
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
}
public class RpcRequestHandler {
private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
}
结果:全村只有一个登记处!
🎯 现在会怎样?
java
// 张三在任何地方登记服务,都是登记在同一个地方
NettyRpcServer server = new NettyRpcServer();
server.registerService("修鞋服务", 张三修鞋店); // 登记在唯一的登记处
// 李四在任何地方找服务,都是在同一个地方找
RpcRequestHandler handler = new RpcRequestHandler();
Object service = handler.getService("修鞋服务"); // 在同一个登记处找,找到了!✅
📊 总结对比表
方式 | 结果 | 问题 | 解决方案 |
---|---|---|---|
new ZkServiceProviderImpl() |
每个类都有自己的服务列表 | 服务注册和查找不在同一个地方 ❌ | 数据不一致,找不到服务 |
SingletonFactory.getInstance() |
全局共享同一个服务列表 | 服务注册和查找在同一个地方 ✅ | 数据一致,能正确找到服务 |
🎮 生活中的类比
错误做法(用new):
java
收银员A: 我有一个商品清单A
收银员B: 我有一个商品清单B
收银员C: 我有一个商品清单C
// 进货员把新商品登记在清单A
// 顾客去收银员B那里买,收银员B的清单里没有 → 说没货!❌
正确做法(用单例):
java
收银员A: 我们都用同一个商品清单
收银员B: 我们都用同一个商品清单
收银员C: 我们都用同一个商品清单
// 进货员把新商品登记在唯一的清单里
// 顾客去任何收银员那里买,都能在同一个清单里找到 → 有货!✅
为什么需要单例?
- 🏪 全应用只能有一个"服务商店"
- 📦 所有服务都要放在同一个货架上
- 🔍 大家找服务时,都要去同一个地方找
4.2 序列化器(Serializer)
java
// 通过SPI + 单例工厂获取序列化器
Serializer serializer = ExtensionLoader
.getExtensionLoader(Serializer.class)
.getExtension("kyro"); // 内部使用SingletonFactory创建
为什么需要单例?
- 🔧 序列化器是无状态工具,一个就够
- 💰 节省内存,避免重复创建
- ⚡ 提升性能,减少对象创建开销
4.3 负载均衡器(LoadBalance)
java
public class ConsistentHashLoadBalanceNew extends AbstractLoadBalance {
@Override
protected String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest) {
// 使用单例创建一致性哈希负载均衡器
// 注释:单例模式创建对象,减少频繁创建对象带来的负载均衡消耗
selector = SingletonFactory.getInstance(
() -> new ConsistentHashingLoadBalancer(serviceAddresses, 160, hashFunction),
ConsistentHashingLoadBalancer.class
);
}
}
为什么需要单例?
- 🔄 维护一致的哈希环状态
- ⚡ 避免频繁重建哈希环
- 💾 减少重复的数据结构
5. 单机单例 vs 集群单例
🖥️ 单机单例(JVM级别)
java
// 这个单例只在一个JVM进程内有效
private final ServiceProvider serviceProvider =
SingletonFactory.getInstance(ZkServiceProviderImpl.class);
作用范围: 一台机器上的一个Java进程
css
机器A (JVM-A) 机器B (JVM-B)
┌─────────────────┐ ┌─────────────────┐
│ ServiceProvider│ │ ServiceProvider│
│ 实例A (单例) │ │ 实例B (单例) │
│ │ │ │
│ 服务列表: │ │ 服务列表: │
│ - 用户服务 │ │ - 订单服务 │
│ - 支付服务 │ │ - 库存服务 │
└─────────────────┘ └─────────────────┘
🌐 集群单例(分布式级别)
要实现集群级别的单例,必须借助第三方组件!
使用ZooKeeper实现集群共享
java
// 所有机器都往同一个ZK集群注册/查找服务
public class ZkServiceRegistryImpl implements ServiceRegistry {
@Override
public void registerService(String serviceName, InetSocketAddress address) {
String servicePath = ZK_ROOT_PATH + "/" + serviceName + address.toString();
CuratorFramework zkClient = CuratorUtils.getZkClient();
CuratorUtils.createPersistentNode(zkClient, servicePath);
}
}
架构图:
bash
┌─────────────────────────────────────────────────────────────────┐
│ ZooKeeper 集群 │
│ (全局服务注册中心) │
│ │
│ /rpc/UserService/192.168.1.100:8080 │
│ /rpc/UserService/192.168.1.101:8080 │
│ /rpc/OrderService/192.168.1.102:8080 │
└─────────────────────────────────────────────────────────────────┘
↑ 注册服务 ↑ 发现服务
│ │
┌─────────────────────┐ ┌─────────────────────┐
│ 机器A (JVM-A) │ │ 机器B (JVM-B) │
│ ServiceProvider │ │ ServiceProvider │
│ (单例实例A) │ │ (单例实例B) │
│ 通过ZK注册到全局 │ │ 通过ZK发现全局服务 │
└─────────────────────┘ └─────────────────────┘
🎯 双重保障机制
层面 | 技术方案 | 作用范围 | 解决问题 |
---|---|---|---|
单机单例 | SingletonFactory |
JVM进程内 | 本机内部数据一致性、性能优化 |
集群共享 | ZooKeeper/Redis |
整个集群 | 跨机器服务发现、负载均衡、故障恢复 |
6. 使用场景判断
🤔 判断是否需要单例,问自己:
1. 这个东西需要"记住"全局信息吗?
- ✅ 是 → 需要单例(如服务管理器、配置管理器)
- ❌ 否 → 看下一条
2. 这个东西是"工具"吗?
- ✅ 是 → 需要单例(如计算器、序列化器)
- ❌ 否 → 看下一条
3. 每次使用都是独立的吗?
- ✅ 是 → 不需要单例(如用户信息、请求对象)
- ❌ 否 → 可能需要单例
📊 场景分类表
组件类型 | 是否单例 | 原因 | 示例 |
---|---|---|---|
无状态工具类 | ✅ 是 | 无状态、线程安全、节省资源 | KryoSerializer |
全局状态管理器 | ✅ 是 | 全局状态管理、避免重复注册 | ZkServiceProviderImpl |
资源管理器 | ✅ 是 | 连接池管理、资源复用 | ThreadPoolFactory |
配置管理器 | ✅ 是 | 全局配置、扩展加载 | ExtensionLoader |
业务数据对象 | ❌ 否 | 每次请求独立状态 | RpcRequest |
临时处理器 | ❌ 否 | 每个连接独立处理 | SocketHandler |
🏠 生活中的类比记忆
生活例子 | 是否单例 | 原因 |
---|---|---|
🏛️ 村委会 | ✅ 是 | 村里只能有一个,管理全村事务 |
🔧 村里的修理工具 | ✅ 是 | 大家共用一套工具就够了 |
📱 每个人的手机 | ❌ 否 | 每个人都有自己的手机 |
🍚 每顿饭 | ❌ 否 | 每顿饭都是独立的 |
7. 最佳实践
✅ 推荐做法
1. 使用单例工厂模式
java
// 推荐:使用统一的单例工厂
ServiceProvider provider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
2. 线程安全的实现
java
// 使用双重检查锁 + volatile
private static volatile MySingleton instance;
3. 延迟初始化
java
// 按需创建,节省资源
public static MySingleton getInstance() {
if (instance == null) {
synchronized (MySingleton.class) {
if (instance == null) {
instance = new MySingleton();
}
}
}
return instance;
}
❌ 避免的做法
1. 不要在构造函数中做重操作
java
// 错误:构造函数中连接数据库
public class BadSingleton {
private BadSingleton() {
connectToDatabase(); // 可能很慢!
}
}
2. 不要忽略线程安全
java
// 错误:线程不安全的懒汉式
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton(); // 多线程问题!
}
return instance;
}
3. 不要滥用单例
java
// 错误:用户数据不应该单例
User user = SingletonFactory.getInstance(User.class); // 错误!
🎯 核心原则
记住这句话:单例就像村里的公共设施,大家都用同一个!
- 🏪 服务注册表 → 必须全局唯一,否则注册和查找会错乱
- 🔧 工具类 → 可以单例,节省内存
- 📱 用户数据 → 不能单例,每个用户都不同
🚀 性能优化建议
- 使用缓存机制:避免重复创建相同对象
- 延迟初始化:按需创建,节省启动时间
- 线程安全:使用高效的同步机制
- 异常处理:创建失败要有明确的错误信息
📝 总结
单例模式是一种重要的设计模式,在RPC框架中广泛应用:
- 🎯 核心思想:全局只有一个实例,大家共享使用
- 🏠 单机应用:通过SingletonFactory保证JVM内唯一
- 🌐 集群应用:通过ZooKeeper等中间件实现分布式协调
- ⚡ 性能优势:节省内存、提升性能、保证数据一致性
最重要的是理解什么时候需要单例:当你需要"全局共享数据"时,必须用单例!