单例模式

单例模式学习笔记

📚 目录

  • [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);  // 错误!

🎯 核心原则

记住这句话:单例就像村里的公共设施,大家都用同一个!

  • 🏪 服务注册表 → 必须全局唯一,否则注册和查找会错乱
  • 🔧 工具类 → 可以单例,节省内存
  • 📱 用户数据 → 不能单例,每个用户都不同

🚀 性能优化建议

  1. 使用缓存机制:避免重复创建相同对象
  2. 延迟初始化:按需创建,节省启动时间
  3. 线程安全:使用高效的同步机制
  4. 异常处理:创建失败要有明确的错误信息

📝 总结

单例模式是一种重要的设计模式,在RPC框架中广泛应用:

  • 🎯 核心思想:全局只有一个实例,大家共享使用
  • 🏠 单机应用:通过SingletonFactory保证JVM内唯一
  • 🌐 集群应用:通过ZooKeeper等中间件实现分布式协调
  • 性能优势:节省内存、提升性能、保证数据一致性

最重要的是理解什么时候需要单例:当你需要"全局共享数据"时,必须用单例!

相关推荐
用户8356290780513 小时前
掌控PDF页面:使用Python轻松实现添加与删除
后端·python
无责任此方_修行中3 小时前
谁动了我的数据?一个 Bug 背后的“一行代码”真凶
后端·node.js·debug
用户47949283569153 小时前
面试官:讲讲2FA 双因素认证原理
前端·后端·安全
疯狂的程序猴3 小时前
移动端H5网页远程调试:WEINRE、Charles与Genymotion完整指南
后端
爱好学习的青年人3 小时前
一文详解Go语言字符串
开发语言·后端·golang
Chan163 小时前
批处理优化:从稳定性、性能、数据一致性、健壮性、可观测性五大维度,优化批量操作
java·spring boot·后端·性能优化·java-ee·intellij-idea·优化
Rexi3 小时前
Go.mod版本号规则:语义化版本
后端
Ray664 小时前
guide-rpc-framework vs Dubbo 实现
后端
Qperable4 小时前
gitlab-runner提示401 Unauthorized
后端·gitlab