php单例在swoole等常驻内存环境中不线程安全,因多协程并发调用getinstance()时判空与赋值非原子操作导致竞态;需用swoole_atomic或redis::setnx实现原子初始化,并禁用__clone和__wakeup防止绕过。

PHP 单例模式本身不保证线程安全,因为 PHP 传统上是无共享内存的进程模型;但在 FPM + 多进程、Swoole/Workerman 等常驻内存场景下,“线程安全”实际要解决的是并发访问下的实例竞争问题。
为什么 getInstance() 在 Swoole 中可能返回多个实例
在 Swoole 的协程或 Worker 进程中,static 变量作用域是进程级(或协程上下文),但若多个协程/请求几乎同时调用 getInstance(),且判断和赋值未加锁,就会触发竞态:两个协程都看到 self::$instance === null,各自 new 一次。
- 典型现象:
var_dump(MySingleton::getInstance() === MySingleton::getInstance()); // false - 只发生在常驻内存环境(Swoole、ReactPHP、PHP-FPM 持久连接等),CLI 或传统 FPM 每次请求都是干净进程,天然“隔离”
- PHP 本身没有
synchronized关键字,不能靠语言特性自动加锁
用 swoole_atomic 或 Redis::setnx() 控制初始化抢占
核心思路:把“判空 → 创建 → 赋值”变成原子操作。不能依赖 if (!self::$instance) { self::$instance = new static(); } 这种非原子写法。
- 在 Swoole 环境下,优先用
swoole_atomic做轻量标记:$inited = new \Swoole\Atomic(0); // ... if ($inited->get() === 0 && $inited->add(1) === 1) { self::$instance = new static(); } - 跨进程(如多 Worker)需外部协调,用
Redis::setnx('singleton:MyClass', 1)配合过期时间,成功者初始化,失败者轮询等待或直接返回已存在的实例 - 注意:
apcu_add()也具备原子性,但仅限本进程有效,不适用于多 Worker 场景
__clone() 和 __wakeup() 必须严格禁止
即使加了初始化锁,反序列化或克隆仍可能绕过单例约束,生成新对象。
- 必须显式抛出异常:
private function __clone() { throw new \Exception("Cannot clone singleton"); } private function __wakeup() { throw new \Exception("Cannot unserialize singleton"); } - 漏掉
__wakeup是高频坑点:当单例被缓存(如 Redis 存了序列化结果),反序列化时会触发它,跳过getInstance() - 如果类继承自框架基类,确认父类没开放
__clone或__wakeup
真正难的不是写一个“看起来像单例”的类,而是想清楚你的运行环境是否共享状态、哪些路径会绕过构造逻辑、以及锁的粒度是否覆盖所有初始化入口——比如数据库连接池、配置管理器这类高频单例,往往在 __construct() 里还藏着网络 IO,这时候光锁住实例变量还不够,得锁住整个初始化流程。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











