PHP单例模式必须私有化__construct、__clone和__wakeup三方法,否则new、clone或unserialize可绕过getInstance()创建新实例,导致===比较失败;静态属性需显式初始化为null,并发场景须加锁防竞态。

PHP单例模式不是“学会写法”就完事,而是先搞清它在哪能用、在哪会崩——多数人卡在“写了但不生效”或“生效了但测试全挂”,根源不在语法,而在对 __construct、__clone、__wakeup 三道门的控制力度不够。
为什么 new 和 clone 会直接绕过 getInstance()
单例失效的第一现场,永远是 $a = new Logger() 或 $b = clone $a 后发现 $a === $b 返回 false。这不是逻辑错,是防护没焊死。
-
__construct必须是private:public/protected 构造函数等于敞开大门,new调用完全不走getInstance() -
__clone同样必须private:否则已有实例可被任意复制,且新对象和原实例内存地址不同,状态完全隔离 - 别信“我只用 getInstance() 就行”——只要类暴露了可调用的构造或克隆入口,别人就能绕
__wakeup 私有化不是可选项,是保命线
反序列化是单例最隐蔽的破口。unserialize($str) 不经过 getInstance(),也不触发 __construct,而是直接重建对象并调用 __wakeup。
- 如果
__wakeup是 public 或 protected,PHP 会静默创建新实例,===比较必败 - 只在
__wakeup里throw异常不够——低版本 PHP 仍可能完成反序列化;必须设为private function __wakeup() - 真要支持序列化(极少见),改用
__serialize()+__unserialize(),并在后者中明确返回self::getInstance()
继承场景下 self:: 和 static:: 的区别直接决定单例是否成立
子类调用 Parent::getInstance() 却拿到父类实例,意味着子类特有方法不可用,单例逻辑已断裂。
立即学习“PHP免费学习笔记(深入)”;
- 写
new self()→ 返回定义getInstance()的类(通常是父类),子类调用也得不到子类实例 - 写
new static()→ 返回实际调用方的类(即子类),配合static $instances = []按类名缓存,父子类各持一份实例 - 静态属性如
$_instance初始值必须显式设为null,不能留空或依赖隐式初始化,否则类型声明(如private static ?self $_instance = null;)会失效
并发首次访问时 static 变量判断根本不可靠
Web 环境下多个请求几乎同时执行 if (self::$instance === null),结果就是多个 new static() 并发发生——你看到的是“单例”,其实是“伪单例”。
- PHP-FPM 多 worker 场景中,每个进程独立,
static变量不跨进程,但单个请求内若含协程(如 Swoole)或长循环,仍存在竞态 - 简单 CLI 脚本可暂不处理;高并发 Web 必须加锁:
flock()文件锁最轻量,Redis 锁适合分布式 - PHP 8.1+ 的
WeakMap不解决此问题——它只管对象生命周期,不管并发控制
真正难的不是写出那几行代码,而是在继承链里保证 $_instance 不被共享、在测试中每次都能干净重置、在反序列化时不让新对象悄悄落地——这些点一旦漏掉一个,单例就从“唯一实例”变成“多个幽灵副本”。



















