JavaScript如何实现方法同步 解决Socket单例重复创建问题
问题根因
重复创建连接的核心原因是异步初始化逻辑产生的竞态:
- 第一次调用
getInstance()时,检测到this.instance为空,进入实例创建流程,但await o.initialize()是异步非阻塞操作,执行到await时会让出JS事件循环 - 在第一次Socket连接尚未建立完成、
this.instance还未被赋值的时间窗口内,第二次getInstance()调用触发,此时检测到this.instance仍然为空,会再次进入实例创建流程,最终生成两个独立的Socket实例 - 两次初始化都执行完成后,
this.instance被后一次创建的实例覆盖,后续调用才会正常复用实例,和你日志中观测到的两次创建成功、第三次才复用的现象完全匹配。
JavaScript是单线程执行模型,不存在Java多线程场景下的资源抢占问题,不需要实现重量级的synchronized同步锁,只要缓存初始化阶段的Promise,让所有并发调用等待同一个初始化流程完成,就能彻底解决这个问题。
修复实现
在类中新增一个静态属性缓存初始化过程的Promise,调整实例判断逻辑为三层:
- 已有完成初始化的实例,直接返回
- 没有已完成实例但存在正在执行的初始化Promise,直接等待该Promise返回结果
- 既没有实例也没有正在执行的初始化,才启动新的初始化流程
修复后的完整代码:
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.
相关产品推荐
相关产品推荐

