单例模式在 PHP 教程里几乎绕不开,但它并不只是“把构造函数设成 private”这么简单。真正需要判断的是:当前对象到底是不是全局唯一、是否值得长期复用、共享后会不会把状态搅乱。下面结合常见场景、优缺点和一段数据库连接示例,拆开看它什么时候适合用,什么时候反而会埋坑。
单例模式到底解决什么问题
简单说,单例模式的目标是保证在整个应用程序生命周期内,某个类只存在一个实例,并且这个类还要提供一个全局访问点,让外部代码在任何时刻都能拿到同一个对象。
这个思路适合两类需求:
- 对象创建成本较高,不希望频繁创建和销毁。
- 业务上确实只需要一个共享实例,多个地方访问同一份状态更合理。
放到 PHP 项目里,常见理解就是:把“只该有一份”的资源集中管理,避免每次调用都重新初始化。
哪些场景更适合用单例
原文列出的几个场景,基本都属于单例模式的经典用法:

- 数据库连接器,例如 mysql、Oracle、sqlServer;
- 缓存或日志组件,例如 file、redis、memcached;
- 应用中的文件锁定对象。
这些对象有一个共同点:它们通常需要被多个模块反复访问,而且初始化过程可能伴随网络连接、文件句柄或系统资源分配。如果每次都 new 一个新对象,性能和资源消耗都不划算。
不过这里要多加一层判断:适合单例的,不是“看起来全局都能用的对象”,而是“确实应该共享同一实例的对象”。如果对象在不同请求、不同用户或不同业务流程里需要保存不同状态,那它就不适合硬做成单例。
单例的优势和常见争议
它的直接好处
- 确保所有对象访问的是同一个实例。
- 实例对象只存在一份,能节约系统资源,减少频繁创建和销毁对象的开销。
对数据库连接、缓存客户端这类底层资源来说,这两个优点都很现实。尤其是当初始化代价不低时,统一复用实例能让代码行为更稳定,也更容易集中管理连接配置。

为什么它也经常被批评
- 不适用于变化的对象。如果同一类型对象在不同用例场景里需要保存不同状态,单例可能导致数据错误,彼此状态互相污染。
- 由于单例模式中没有抽象层,因此单例类的扩展会比较困难。
- 单例类承担了“控制实例数量”和“提供业务能力”两类职责,在一定程度上违背单一职责原则。
- 滥用单例会带来负面问题。比如为了节省资源,把数据库连接池对象设计成单例,可能因为共享方过多而出现连接池溢出;如果实例长时间不被利用,还可能带来状态管理上的额外问题。
原文里提到,部分“缺点”未必绝对成立,这个判断其实有道理。单例模式本身不是错,问题往往出在场景选择上。对象天然应该全局唯一时,单例可以简化管理;对象本来就需要隔离状态时,单例就会变成问题源头。
因此,与其把它简单归为“反面模式”,不如说它是一种要谨慎使用的模式:只在边界足够清晰、共享语义明确时使用,效果才会稳定。
PHP 示例:用单例管理数据库连接
下面这段代码以数据库连接为例,展示单例模式的基本写法。核心目的很明确:多次调用时,只建立一次数据库连接。
<?php
namespace appdatabasedriver;
class Mysql
{
/**
* @var Mysql
*/
private static $instance;
/**
* @var PDO
*/
private $connection;
/**
* 通过懒加载获得实例(在第一次使用的时候创建)
*/
public static function getInstance()
{
if (null === static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
/**
* private,不允许从外部调用以防止创建多个实例
* 要使用单例,必须通过 Connection::getInstance() 方法获取实例
*/
private function __construct()
{
}
/**
* 获取数据库连接
* @return PDO
*/
public function getConnection()
{
if (null === $this->connection) {
$this->connection = new PDO("mysql:host=127.0.0.1;port=3306;dbname=test;charset=utf8", 'root', 'abc-123');
}
return $this->connection;
}
/**
* 防止实例被克隆(这会创建实例的副本)
*/
private function __clone()
{
}
/**
* 防止反序列化(这将创建它的副本)
*/
private function __wakeup()
{
}
}
这段实现里,关键点有哪些
private static $instance;:保存当前类的唯一实例。getInstance():对外提供统一入口,并通过懒加载方式在第一次使用时创建对象。private function __construct():禁止从类外直接 new,避免产生多个实例。private function __clone()和private function __wakeup():分别阻止克隆和反序列化复制实例。private $connection;与getConnection():把 PDO 连接也做成延迟创建,只有第一次真正访问数据库时才建立连接。
也就是说,这里其实做了两层控制:第一层是 Mysql 类本身只保留一个实例;第二层是实例内部的 PDO 连接也只初始化一次。这样外部无论调用多少次,拿到的都是同一个连接对象。
调用方式
<?php
use appdatabasedriverMysql;
$db = Mysql::getInstance()->getConnection();
// 无论调用多少次,始终仅连接一次数据库
$db = Mysql::getInstance()->getConnection();
$db = Mysql::getInstance()->getConnection();
$statement = $db->query("SHOW databases");
$res = $statement->fetchAll();
这段调用代码表达的重点很直接:Mysql::getInstance()->getConnection() 可以被重复调用,但数据库连接不会被反复创建。对“单一连接、反复使用”的需求来说,这正是单例模式最常见的落点。
单例该不该用,判断标准是什么
很多文章会把单例模式直接贴上 Anti Pattern 的标签,但实际开发里更重要的是判断边界,而不是简单站队。
可以优先问自己这几个问题:
- 这个对象是否真的应该全局唯一?
- 多个模块共享它时,状态是否会互相干扰?
- 它是不是属于连接、日志、缓存、锁这类“集中管理更合理”的资源对象?
- 后续是否需要替换实现、扩展能力或做单元测试隔离?
如果答案偏向“唯一、共享、低变化”,单例通常能成立;如果答案偏向“多状态、强变化、需要灵活替换”,那就应该谨慎,甚至改用依赖注入或普通对象管理方式。
对这篇示例来说,数据库连接属于比较典型的单例适用对象,因此示例本身没有问题。真正需要警惕的,是把所有“看起来常用”的类都做成单例,最终让全局状态越来越重,代码耦合越来越高。













