PHP单例模式核心在于防绕过:__construct必须private以防new创建;__clone和__wakeup也须private,避免克隆与反序列化生成新实例;并发下需用Redis锁或atomic保证getInstance原子性;继承时应使用static::而非self::确保子类实例唯一。
PHP单例模式不是“写个
就完事”,它在并发、继承、反序列化等场景下极易失效。核心问题不在于“怎么写”,而在于“怎么防绕过”。
为什么
必须是private
PHP 不会阻止你
,除非你明确把它设为
。一旦构造函数是
或
,任何地方都能直接 new 出新实例,
就形同虚设。
错误现象:
继承场景下更危险:子类若没重写
,父类
构造函数仍可被调用
别依赖文档或约定——必须靠语言机制锁死,
是第一道也是最硬的一道防线
和
为什么都要private
克隆和反序列化是两个最常见的单例绕过入口。PHP 在执行
或
时,会跳过
,直接调用构造逻辑(或重建对象)。
设为
:否则
就能拿到第二个独立实例
不能只抛异常,必须设为
:否则
会无视异常,静默创建新对象(PHP 8.1+ 行为更严格,但低版本仍存在风险)
注意:如果类里用了
,也要确保它不暴露敏感状态,否则反序列化可能恢复出非预期结构
并发请求下
为何会创建多个实例
纯静态变量判断 + new 的写法(
)在高并发时大概率失效——多个请求几乎同时判断为
,然后各自 new 一次。
PHP 8.5.5
PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。
下载
立即学习
“
PHP免费学习笔记(深入)
”;
PHP-FPM 每个 worker 是独立进程,
变量不跨进程,但在单个请求内(如长脚本、Swoole 协程)仍可能因竞态出问题
PHP 7.4+ 可用
扩展做轻量级锁,但多数项目更稳妥的做法是加文件锁或 Redis 锁(尤其涉及数据库连接等共享资源时)
简单项目若确定无并发首次访问(如 CLI 脚本),可暂不处理;但 Web 场景默认应按“可能并发”设计
继承单例时该用
还是
子类调用
时,必须返回子类自身实例,而不是父类实例。这直接决定能否调用子类特有方法。
错用
:
→ 始终 new 父类,子类方法不可达
正确用
:
→ 实际 new 的是最终调用者类(如
就 new
)
静态属性也得声明为
,否则子类会共享父类的
,导致类型错乱
真正难的不是写出第一个单例,而是当它被用在 CLI 命令、Swoole Worker、单元测试 mock、甚至反序列化 payload 中时,还能守住“唯一性”。每多一个运行环境,就要多检查一道绕过路径。
getInstance()__constructnew DatabaseConnection()privatepublicprotectedgetInstance()$a = new DatabaseConnection(); $b = DatabaseConnection::getInstance(); var_dump($a === $b); // false__constructpublicprivate function __construct()__clone__wakeupclone $objunserialize($str)getInstance()__clone()private$b = clone $a;__wakeup()privateunserialize()__sleep()getInstanceif (self::$instance === null) { self::$instance = new self(); }nullstaticatomicself::static::getInstance()self::self::$instance = new self();static::static::$instance = new static();MyDB::getInstance()MyDBstatic::$instance$instance