单例模式:为什么大模型只加载一次就够了?

从 Popup 弹窗到 WebGPU 推理管线,同一个设计模式在不同场景下的优雅实现。

一、一个场景开场

假设你做了一个按钮,点击后弹出新窗口。如果用户狂点按钮,每次 new Popup() 都会创建一个新窗口 ------ 这就炸了。

html 复制代码
<!-- ❌ 每次点击都 new 一个新实例,弹出一堆窗口 -->
<button id="openBtn">打开新网页</button>
<script>
  class Popup {
    open(url) {
      window.open(url, '_blank');
    }
  }
  document.getElementById("openBtn").addEventListener("click", () => {
    new Popup().open("https://www.baidu.com");  // 每次点都 new
  });
</script>

问题:同一个功能,为什么要创建多个对象?我们希望全局只有一个 Popup 实例,大家共用。

这就是**单例模式(Singleton Pattern)**的用武之地。

html 复制代码
<button id="openBtn">打开新网页</button>
<script>
  class Popup {
    static ins;  // 静态属性,属于类本身,不属于实例

    static getInstance() {
      if (!Popup.ins) {
        Popup.ins = new Popup();  // 第一次调用才 new
      }
      return Popup.ins;  // 后续调用直接返回已有的
    }

    open(url) {
      window.open(url, '_blank');
    }
  }

  // 验证:两次获取的是同一个实例吗?
  const a = Popup.getInstance();
  const b = Popup.getInstance();
  console.log(a === b);  // true

  // 使用
  const openBtn = document.getElementById("openBtn");
  openBtn.addEventListener("click", () => {
    a.open("https://www.baidu.com");
  });
</script>

拆解每一行

代码 含义
static ins 静态属性,挂在类 Popup 上,而不是实例上。所有地方通过 Popup.ins 访问的都是同一个值
static getInstance() 静态方法,不依赖实例就能调用
if (!Popup.ins) 判断是否已经创建过实例,只创建一次
Popup.ins = new Popup() 第一次调用时创建实例,并缓存到静态属性上
return Popup.ins 后续调用直接返回缓存的实例

这就是单例模式的全部精髓:类只实例化一次,全局只有一个实例

三、静态属性 vs 实例属性:一张表讲清楚

对比维度 实例属性 静态属性
语法 this.xxx static xxx
归属 属于 new 出来的实例对象 属于类本身
访问方式 new A().xxx A.xxx
不需要 new 就能访问?
内存 每个实例各一份 全局唯一一份
典型场景 组件内部状态 全局配置、单例实例、工具方法

类比理解:

js 复制代码
class Person {
  static species = "人类";  // 所有人共享,属于 "Person" 这个类
  constructor(name) {
    this.name = name;      // 每个人不同,属于具体的人
  }
}

const xiaoming = new Person("小明");
const xiaohong = new Person("小红");

console.log(xiaoming.name);      // "小明" --- 实例属性,各不同
console.log(xiaohong.name);      // "小红"
console.log(Person.species);     // "人类" --- 静态属性,全局唯一

四、进阶:单例模式在 WebGPU DeepSeek 项目中的实战

回到之前聊过的 WebGPU DeepSeek R1 项目,Worker 线程里也有一个单例:

js 复制代码
class TextGenerationPipeline {
  static model_id = "onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";

  static async getInstance(progress_callback = null) {
    // ??= 逻辑空赋值:只有 this.tokenizer 为 null/undefined 时才执行
    this.tokenizer ??= AutoTokenizer.from_pretrained(this.model_id, {
      progress_callback,
    });
    return Promise.all([this.tokenizer]);
  }
}

// 使用:多次调用 getInstance,实际只下载一次
const [tokenizer] = await TextGenerationPipeline.getInstance(progressCallback);

为什么这里必须用单例?

  1. 模型文件巨大(GB 级别),下载需要几分钟甚至更久
  2. 内存昂贵:模型加载到内存后占用大量空间,绝不能加载两份
  3. 多次调用:用户可能多次点击"生成",每次都应该复用同一个已加载的模型

??= 运算符:单例的现代写法

js 复制代码
// 传统写法
if (this.tokenizer === null || this.tokenizer === undefined) {
  this.tokenizer = await AutoTokenizer.from_pretrained(...)
}

// ??= 写法(等价,更简洁)
this.tokenizer ??= await AutoTokenizer.from_pretrained(...)

??= 只忽略 nullundefined,不忽略 0""false,比 ||= 更精确。这是单例模式在现代 JavaScript 中最优雅的实现方式。

五、单例模式的其他应用场景

场景 为什么用单例
全局状态管理(如 Redux Store) 整个应用只有一个 Store
数据库连接池 连接是昂贵的资源,全局复用
日志记录器 所有模块写入同一个日志
配置管理 全局配置只读一份
浏览器缓存管理 缓存空间全局共享
WebSocket 连接 和服务端保持一条长连接就够了

六、单例模式的优缺点

优点

  • 全局唯一:天然保证实例唯一性
  • 节省资源:避免重复创建的 CPU/内存开销
  • 统一入口:所有调用方通过同一个方法获取实例,便于管理

缺点

  • 全局状态:相当于全局变量,可能被任意代码修改,难以追踪
  • 测试困难:单例持有状态,单元测试之间可能互相影响
  • 强耦合:调用方依赖具体的类,难以替换实现

最佳实践:单例模式适合真正的「全局唯一资源」场景(如模型实例、数据库连接),不要为了省事把所有东西都搞成单例。

七、总结

问题 答案
单例模式是什么? 保证一个类只有一个实例,并提供全局访问点
核心实现? static 属性 + getInstance() 方法 + 懒加载判断
和 new 的区别? new 每次创建新实例,getInstance() 永远返回同一个
什么时候用? 全局唯一资源:模型实例、数据库连接、配置管理、日志服务
什么时候不用? 需要多实例的场景、需要频繁创建销毁的对象、单元测试频繁的场景

一句话 :单例模式解决的是「这个资源全局只需要一份」的问题。从 Popup 弹窗到 GB 级大模型,思路完全一样:第一次创建,后面复用,static + getInstance() + 判空逻辑,三行搞定。这也是 OOP 23 种设计模式中最经典、最常用的一种。理解了它,你就理解了「面向设计,而不是面向实现」的编程思想。

相关推荐
不正经学生19 分钟前
C语言动态内存管理(上):堆上的自由与责任
c语言·开发语言·c++·算法·面试
ssshooter1 小时前
AI 时代你不能不知道的 git worktree
前端·后端·面试
CoderYanger1 小时前
A.每日一题:输入单词需要的最少按键次数 Ⅰ+Ⅱ
java·数据结构·算法·leetcode·面试
艾莉丝努力练剑6 小时前
【AI大模型接入SDK】SSE协议
c++·学习·面试·大模型·sdk
Rain的Java大神之路6 小时前
如何保证接口幂等
java·经验分享·后端·面试·架构
aichitang20246 小时前
快乐泛函每一天:希尔伯特空间中的最佳逼近与正交投影定理
人工智能·考研·算法·ai·面试·泛函分析
艾莉丝努力练剑7 小时前
【AI大模型接入SDK】WebSocket & SSE 协议
网络·c++·websocket·网络协议·学习·面试
小义_7 小时前
JDK 深度解析
java·linux·开发语言·python·面试
数智启示录7 小时前
Doris精讲篇(六) 从第一批小文件到 -235:Doris 是怎样被正常写入拖死的
大数据·数据库·经验分享·面试·flink
RisunJan7 小时前
Android 面试知识点整理
android·面试·职场和发展