You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript如何实现方法同步 解决Socket单例重复创建问题

问题根因

重复创建连接的核心原因是异步初始化逻辑产生的竞态:

  • 第一次调用getInstance()时,检测到this.instance为空,进入实例创建流程,但await o.initialize()是异步非阻塞操作,执行到await时会让出JS事件循环
  • 在第一次Socket连接尚未建立完成、this.instance还未被赋值的时间窗口内,第二次getInstance()调用触发,此时检测到this.instance仍然为空,会再次进入实例创建流程,最终生成两个独立的Socket实例
  • 两次初始化都执行完成后,this.instance被后一次创建的实例覆盖,后续调用才会正常复用实例,和你日志中观测到的两次创建成功、第三次才复用的现象完全匹配。

JavaScript是单线程执行模型,不存在Java多线程场景下的资源抢占问题,不需要实现重量级的synchronized同步锁,只要缓存初始化阶段的Promise,让所有并发调用等待同一个初始化流程完成,就能彻底解决这个问题。

修复实现

在类中新增一个静态属性缓存初始化过程的Promise,调整实例判断逻辑为三层:

  1. 已有完成初始化的实例,直接返回
  2. 没有已完成实例但存在正在执行的初始化Promise,直接等待该Promise返回结果
  3. 既没有实例也没有正在执行的初始化,才启动新的初始化流程

修复后的完整代码:

import io from 'socket.io-client';

export default class SocketIo {
  static socket = null;
  static instance = null;
  // 缓存初始化Promise,解决并发调用的竞态问题
  static #initPromise = null;

  async initialize() {
    this.socket = await io(`http://${ip}:10300/`, {
      transports: ['websocket'],
    });
  }

  static async getInstance() {
    logger.info('socketIo.api.getInstance: BEGIN');
    // 已有实例直接复用
    if (this.instance) {
      logger.info('socketIo.api.getInstance: a socket instance already exists, reusing that one');
      logger.info('socketIo.api.getInstance: END');
      return this.instance;
    }
    // 无正在进行的初始化任务时,才启动新的初始化
    if (!this.#initPromise) {
      logger.info('socketIo.api.getInstance: creating new socket instance...');
      this.#initPromise = (async () => {
        try {
          const o = new SocketIo();
          await o.initialize();
          this.instance = o;
          logger.info('socketIo.api.getInstance: socket instance created SUCCESSFULLY');
          this.#initPromise = null;
          return this.instance;
        } catch (e) {
          // 初始化失败时清空Promise缓存,允许后续调用重试
          this.#initPromise = null;
          moaLog('socketIo.api.getInstance: ERROR: ', e);
          throw e;
        }
      })();
    }
    // 所有并发调用统一等待同一个初始化任务完成
    const res = await this.#initPromise;
    logger.info('socketIo.api.getInstance: END');
    return res;
  }
}

如果你的项目构建环境不支持ES标准私有属性(#前缀属性),可以把#initPromise替换为普通静态属性_initPromise,只要避免在类外部手动修改该属性值即可。

调用修正

原有main.js中的调用写法存在错误:getInstance()是async异步方法,直接赋值拿到的是Promise对象而非最终的Socket实例,必须使用await或.then()获取实例:

// 正确调用方式
const socket1 = await SocketIo.getInstance();
const socket2 = await SocketIo.getInstance();

// 间隔一段时间后调用
const socket3 = await SocketIo.getInstance();

修复后无论短时间内触发多少次getInstance()调用,都只会建立一次WebSocket连接,所有调用都会拿到同一个单例对象。


内容的提问来源于stack exchange,提问作者Gianluca B.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 06:06:40