单例模式:概念与结构、PHP实现、在数据库连接池中的应用、线程安全问题
各位同学,今天我们来聊聊单例模式。说实话,这个模式是我在项目中用得最多的一个,没有之一。为什么?因为太实用了。你想想看,一个数据库连接、一个日志写入器、一个配置管理器——这些东西在系统里只需要一份就够了,多了反而出乱子。
什么是单例模式?
单例模式,说白了就是保证一个类在整个应用生命周期里只有一个实例。不管你 new 多少次,拿到的都是同一个对象。我刚开始写 PHP 的时候,总觉得这玩意儿有点多余——我少 new 几次不就行了?后来在真实项目里被坑过才明白,光靠自觉是管不住的。
单例模式的核心就三点:
- 私有构造函数——不让外部直接 new
- 静态方法——提供一个全局访问点
- 静态属性——保存唯一的实例
嗯,结构其实很简单,但背后的设计思想值得琢磨。
PHP 实现:一步一步来
我们先看一个最基础的单例实现。我个人习惯把单例类命名为 Singleton,但实际项目中你肯定不会这么叫,而是叫 Database、Logger 之类的。
<?php
class Singleton
{
// 静态属性保存唯一实例
private static ?self $instance = null;
// 私有构造函数,防止外部 new
private function __construct()
{
// 初始化操作,比如连接数据库
echo "实例已创建\n";
}
// 私有克隆方法,防止克隆
private function __clone() {}
// 私有反序列化方法,防止反序列化
public function __wakeup()
{
throw new \Exception("不能反序列化单例");
}
// 静态方法获取实例
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function doSomething(): void
{
echo "执行一些操作\n";
}
}
// 使用
$obj1 = Singleton::getInstance();
$obj2 = Singleton::getInstance();
var_dump($obj1 === $obj2); // 输出 true
?>
这里有几个细节要注意:
__clone()私有化——防止别人用 clone 复制对象__wakeup()抛出异常——防止反序列化破坏单例- 构造函数私有——这是最根本的保障
在数据库连接池中的应用
好了,理论说完了,咱们来点实战。数据库连接池是单例模式最经典的应用场景。为什么?因为数据库连接是稀缺资源,每个连接都要消耗内存和网络资源。如果每个请求都 new 一个连接,服务器很快就扛不住了。
我参与过一个电商项目,初期没做连接池,结果双十一那天数据库连接数直接爆了。后来重构时,我用单例模式实现了一个简单的连接池,效果立竿见影。
<?php
class DatabasePool
{
private static ?self $instance = null;
private array $connections = [];
private int $maxConnections = 10;
private int $currentConnections = 0;
private function __construct(int $maxConnections = 10)
{
$this->maxConnections = $maxConnections;
}
public static function getInstance(int $maxConnections = 10): self
{
if (self::$instance === null) {
self::$instance = new self($maxConnections);
}
return self::$instance;
}
public function getConnection(): PDO
{
// 先从池子里找空闲连接
foreach ($this->connections as $key => $conn) {
if ($conn['busy'] === false) {
$this->connections[$key]['busy'] = true;
return $conn['pdo'];
}
}
// 没有空闲连接,创建新连接
if ($this->currentConnections < $this->maxConnections) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$this->connections[] = [
'pdo' => $pdo,
'busy' => true
];
$this->currentConnections++;
return $pdo;
}
// 连接池满了,等待或抛出异常
throw new \Exception("连接池已满,请稍后重试");
}
public function releaseConnection(PDO $pdo): void
{
foreach ($this->connections as $key => $conn) {
if ($conn['pdo'] === $pdo) {
$this->connections[$key]['busy'] = false;
return;
}
}
}
private function __clone() {}
public function __wakeup()
{
throw new \Exception("不能反序列化连接池");
}
}
// 使用示例
$pool = DatabasePool::getInstance(20);
$conn = $pool->getConnection();
// ... 执行数据库操作
$pool->releaseConnection($conn);
?>
线程安全问题
说到线程安全,很多 PHP 开发者会觉得跟自己没关系——PHP 不是单线程的吗?嗯,这话对了一半。
传统的 PHP-FPM 模式下,每个请求是独立的进程,确实不存在线程安全问题。但如果你在用 Swoole、Workerman 这类常驻内存的框架,或者 PHP 的 pthreads 扩展,那就要小心了。
为什么会这样?因为单例的静态属性是共享的。在多线程环境下,两个线程可能同时判断 $instance === null,然后同时创建实例,这就破坏了单例。
我曾在 Swoole 项目里遇到过这个问题。当时线上偶尔会出现数据库连接混乱的情况,查了好久才发现是单例被重复创建了。
解决方案其实不复杂:
<?php
class ThreadSafeSingleton
{
private static ?self $instance = null;
private static bool $locked = false;
public static function getInstance(): self
{
// 双重检查锁定
if (self::$instance === null) {
if (!self::$locked) {
self::$locked = true;
if (self::$instance === null) {
self::$instance = new self();
}
self::$locked = false;
} else {
// 等待锁释放
usleep(100);
return self::getInstance();
}
}
return self::$instance;
}
private function __construct() {}
private function __clone() {}
public function __wakeup()
{
throw new \Exception("不能反序列化");
}
}
?>
总结一下
单例模式看着简单,但用好了能解决大问题。我总结了几条经验:
- 适用场景: 数据库连接、日志写入、配置管理、缓存服务
- 不适用场景: 需要多实例的对象、状态频繁变化的对象
- 避坑指南: 记得处理 clone 和反序列化,多线程环境加锁
最后说一句,单例模式虽然好用,但别滥用。我见过有人把所有的类都写成单例,结果代码耦合得一塌糊涂。记住,单例是为了解决「只需要一个实例」的问题,不是为了方便调用。
好了,这一章就到这里。下一章我们聊聊工厂模式,到时候我会分享一个我在支付系统里用工厂模式重构的案例,很有意思。