单例模式:概念与结构、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() 抛出异常——防止反序列化破坏单例
  • 构造函数私有——这是最根本的保障
我曾经踩过的坑: 有一次我忘了把 __clone 私有化,结果团队里有人用 clone 复制了单例对象,导致数据库连接被重复创建,线上出了严重的性能问题。从那以后,我写单例一定会把这三个方法都处理干净。

在数据库连接池中的应用

好了,理论说完了,咱们来点实战。数据库连接池是单例模式最经典的应用场景。为什么?因为数据库连接是稀缺资源,每个连接都要消耗内存和网络资源。如果每个请求都 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("不能反序列化");
    }
}
?>
我的建议: 如果你用的是传统 PHP-FPM,不用太担心线程安全。但如果你开始接触 Swoole 或 Workerman,一定要把线程安全放在心上。我个人的习惯是,不管什么场景,都按线程安全的方式写单例——养成好习惯,省得以后踩坑。

总结一下

单例模式看着简单,但用好了能解决大问题。我总结了几条经验:

  • 适用场景: 数据库连接、日志写入、配置管理、缓存服务
  • 不适用场景: 需要多实例的对象、状态频繁变化的对象
  • 避坑指南: 记得处理 clone 和反序列化,多线程环境加锁

最后说一句,单例模式虽然好用,但别滥用。我见过有人把所有的类都写成单例,结果代码耦合得一塌糊涂。记住,单例是为了解决「只需要一个实例」的问题,不是为了方便调用。

好了,这一章就到这里。下一章我们聊聊工厂模式,到时候我会分享一个我在支付系统里用工厂模式重构的案例,很有意思。