msgbartop
PHP语言, PHP扩展, Zend引擎相关的研究,技术,新闻分享 – 左手代码 右手诗
msgbarbottom

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

在上一篇文章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;
    }
}


分享到:



Related Posts:

Tags: , , , , , ,

11 Responses to “可序列化单例模式的遗留问题答案”

  1. cdy |

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

  2. 可序列化单例模式的遗留问题答案树林/咖啡 成都专业php网站制作 | 树林/咖啡 成都专业php网站制作 |

    [...] 风雪之隅 » PHP源码分析 Posted in: php / Tagged: 可序列化单例模式的遗留问题答案 [...]

  3. mssql |

    good

  4. 安然 |

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

  5. miao |

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

  6. miao |

    $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的单例?

  7. Thinking In LAMP Blog » Blog Archive » PHP每月通讯(2011年4月) |

    [...] http://www.laruence.com/2011/03/18/1916.html   可序列化单例模式的遗留问题答案 [...]

  8. 可序列化单例模式的遗留问题答案 | 万维网黑客联盟 |

    [...] 本文地址: http://www.laruence.com/2011/03/18/1916.html [...]

  9. willko |

    这样写还是有bug的。

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

    理想的方案是是当unserialize返回已有的对象,

    public function __wakeup() {
    return self::$instance;
    }

    可惜不支持return。。。

  10. fifsky |

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

  11. Serialize/Unserialize破坏单例 | 风雪之隅 |

    [...] //bool(false) 大家可以想想为什么这样,, 如果不想想的, 就看我的下一篇文章吧. 最后, 做个广告,,,, 在新浪微博关注我吧: http://t.sina.com.cn/laruence, Related [...]

Leave a Reply

*