Press "Enter" to skip to content

可序列化单例模式的遗留问题答案

在上一篇文章Serialize/Unserialize破坏单例的最后, 我留下了一个问题, 为了让大家能思考, 我就单独再写一篇给出答案.
上一篇中, 我们说到, 为了实现一个支持序列化的单例模式, 我们采用了如下的定义方式:

class Singleton {
    private static $instance = NULL;
    /** 不容许直接调用构造函数 */
    private function __construct() {
    }
    /** 不容许深度复制 */
    private function __clone() {
    }
    public  function __wakeup() {
        self::$instance = $this;
    }
    /** 需要在单利切换的时候做清理工作 */
    public function __destruct() {
        //清理工作
        ....
        self::$instance = NULL;
    }
    public static function getInstance() {
        if (NULL === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

但这样的看似正确的代码, 确在某些时候达不到我们想要的结果:

$a = Singleton::getInstance();
$a = unserialize(serialize($a));
var_dump($a === Singleton::getInstance());
//bool(false)

那么为什么呢?

我之前的文章深入理解PHP原理之变量分离/引用(Variables Separation)中曾经介绍过, 在PHP中, 采用引用计数的方式来减少对内存的使用和提高效率.
回头来看这个问题, 根据运算符的结合律, 我们来单步分析这个过程:
在我们调用unserialize(serialize($a))的时候, 在serialize之前, PHP会首先尝试调用我们的类的实例$a的__sleep方法, 因为我们没有定义此方法, 所以跳过此步骤..
接下来, 在unserialize的时候, PHP在完成对象的创建以后, 会来调用新创建对象的__wakeup方法, 在这里面, 我们释放了原有的self::$instance的引用, 改变成了新的对象.
这个时候, 原来的$a, 并不会被释放, 因为此时符号名a还保留着对$a(单列类的一个实例)的引用, 但此时$a所指的对象的引用计数已经-1, 变成了1, (应该还要了解到, 此时, 还会对Object Store中的对象引用计数-1, 也变为了1)
最后, 我们把得到的新对象给$a赋值, OK, 关键的时候来了, 这个时候, 因为我们重新对$a赋值, 所以$a会释放之前所值向的zval的引用, 造成了此时这个zval的引用计数变为了零, 于是PHP就会释放这个zval, 也就会调用了Singleton的析构函数, 在这个析构函数中, 我们释放了静态实例$instance..
现在明白了么?
当然, 最后写成这样:

class Singleton {
    private static $instance = NULL;
    /** 不容许直接调用构造函数 */
    private function __construct() {
    }
    /** 不容许深度复制 */
    private function __clone() {
    }
    public  function __wakeup() {
        self::$instance = $this;
    }
    /** 需要在单利切换的时候做清理工作 */
    public function __destruct() {
        //只做清理工作.
    }
    public static function getInstance() {
        if (NULL === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

12 Comments

  1. cdy
    cdy March 25, 2013

    你好,我想了解一下,php 会不会像其他语言(C# JAVA),出现线程同步的问题?
    是否要加锁?

  2. mssql
    mssql August 25, 2011

    good

  3. 安然
    安然 June 7, 2011

    用工厂来模型进行单例处理,用类自身的方法太麻烦了,通用性也不好。

  4. miao
    miao April 30, 2011

    补充一下,serialize时也不调用__destruct()的,和unserialize不调用__construct()保持了一致;同时serialize的时候也没有释放原有对象;

  5. miao
    miao April 30, 2011

    $a = Singleton::getInstance();
    $b = Singleton::getInstance();
    //这是$a和$b是同一个对象的引用,单例
    $b = serialize($b);
    //$a 保持不变,$b为字符串
    $b = unserialize($b);
    //另外一个对象了
    结论:
    1、$a和$b依然是两个object;
    2、__destruct()中的self::$instance = NULL;无作用;
    3、__warkup()中的self::$_instance = $this;改变了Singleton::$instance的指向,原本是指向$a的,现在指向了$b;
    4、unserialize是不调用__construct()的
    问题:
    不知道怎么实现支持serialize的单例?

  6. willko
    willko March 19, 2011

    这样写还是有bug的。
    $ex = Singleton::getInstance();
    $now = unserialize(serialize($ex));
    var_dump($ex === $now);
    //bool(false)
    理想的方案是是当unserialize返回已有的对象,
    public function __wakeup() {
    return self::$instance;
    }
    可惜不支持return。。。

  7. fifsky
    fifsky March 18, 2011

    恩,受教了,博主研究很深入啊

Comments are closed.