凡是讲到设计模式,无一例外的都会讲到单例模式,单例模式相对于其他设计模式来讲,要容易理解的多,但是要实现一个严格意义上的单例模式,很简单吗?

很多人可以轻松的写出如下php实现的单例模式:

[php]  ​​view plain​​​  ​​​copy​


1. <?php
2.
3. class Singleton {
4. //保存类实例的静态成员变量
5. private static $_instance;
6.
7. //private 构造函数
8. private function __construct() {
9. echo " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
10. }
11.
12. private function __clone() {
13. echo " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
14. }
15.
16. //单例方法访问类实例
17. public static function getInstance() {
18. if (!(self::$_instance instanceof self)) {
19. $_instance = new self();
20. }
21. return self::$_instance;
22. }
23. }

在该示例中,将构造方法设为private从而防止直接new一个对象;将__clone方法设为private,防止通过clone复制一个对象;需要该类对象"只能"通过调用Singleton::getInstance()方法的方式,而getInstance方法通过"饿汉模式"保证类变量$_instance只会被初始化一次,即Singleton类只能有一个对象。

这种实现方式看似没有问题,当我们试图 new Singleton()或者clone 一个对象时都会发生fatal error。那么,这种方式是否就能保证单例了?并不是。

考虑反射

构造方法被private了,是不是就无法实例化一个类了?来看ReflectionClass的一个方法

ReflectionClass::newInstanceWithoutConstructor — 创建一个新的类实例而不调用它的构造函数

也就是通过这个方法可以不经过构造方法就创建一个对象,上例中试图将构造方法private来阻止实例对象的方法失效了。下面来验证可行性。

为了方便验证,会在上例中加入一些属性及方法。



[php]  ​​view plain​​​  ​​​copy​



1. <?php
2.
3. class Singleton {
4. //保存类实例的静态成员变量
5. private static $_instance;
6. private $_serialize_id = 1234567890;
7.
8. //private 构造函数
9. private function __construct() {
10. $this->setSerializeId(rand(1,1000000000000));
11. echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
12. }
13.
14. private function __clone() {
15. echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
16. }
17.
18. /**
19. * @return mixed
20. */
21. public function getSerializeId() {
22. return $this->_serialize_id;
23. }
24.
25. /**
26. * @param mixed $serialize_id
27. */
28. public function setSerializeId($serialize_id) {
29. $this->_serialize_id = $serialize_id;
30. }
31.
32. //单例方法访问类实例
33. public static function getInstance() {
34. if (!(self::$_instance instanceof self)) {
35. $_instance = new self();
36. }
37. return self::$_instance;
38. }
39. public function __toString()
40. {
41. return __CLASS__ . " " . $this->getSerializeId() ;
42. }
43. }

测试用例脚本:


[php]  ​​view plain​​​  ​​​copy​



    1. <?php
    2. require_once 'singleton.php';
    3.
    4. //$obj1 and $obj3 is the same object
    5.
    6. $obj1 = Singleton::getInstance();
    7. $obj3 = Singleton::getInstance();
    8.
    9. //$obj2 is a new object
    10. $class = new ReflectionClass('Singleton');
    11. $obj2 = $class->newInstanceWithoutConstructor();
    12. $ctor = $class->getConstructor();
    13. $ctor->setAccessible(true);
    14. $ctor->invoke($obj2);
    15.
    16. echo "obj1 equal to obj3: " . ($obj1 === $obj3) . "\n";
    17. echo "obj1 not equal obj2: " . ($obj1 !== $obj2) . "\n";
    18.
    19. xdebug_debug_zval('obj1');
    20. xdebug_debug_zval('obj2');
    21. xdebug_debug_zval('obj3');

    输出case:


    [plain]  ​​view plain​​​  ​​​copy​



      1. Singleton 840562594589 I'm construct! process id is 30019 and thread id is 140410609465280
      2. Singleton 920373440721 I'm construct! process id is 30019 and thread id is 140410609465280
      3. obj1 equal to obj3: 1
      4. obj1 not equal obj2: 1
      5. obj1: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=840562594589 }
      6. obj2: (refcount=1, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=920373440721 }
      7. obj3: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=840562594589 }


      可以看出$obj1和$obj3是同一个对象,而与$obj2则是不同的对象。违反了单例模式。


      考虑序列化


      [php]  ​​view plain​​​  ​​​copy​


      输出case:

      1. <?php
      2. require_once 'singleton.php';
      3.
      4. //$obj1 and $obj3 is the same object
      5.
      6. $obj1 = Singleton::getInstance();
      7. $obj3 = Singleton::getInstance();
      8.
      9. //$obj2 is a new object
      10. $objSer = serialize($obj1);
      11. $obj2 = unserialize($objSer);
      12.
      13. echo "obj1 equal to obj3: " . ($obj1 === $obj3) . "\n";
      14. echo "obj1 not equal obj2: " . ($obj1 !== $obj2) . "\n";
      15.
      16. xdebug_debug_zval('obj1');
      17. xdebug_debug_zval('obj2');
      18. xdebug_debug_zval('obj3');


      [plain]  ​​view plain​​​  ​​​copy​



      1. Singleton 165926147718 I'm construct! process id is 6849 and thread id is 139844633716672
      2. obj1 equal to obj3: 1
      3. obj1 not equal obj2: 1
      4. obj1: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }
      5. obj2: (refcount=1, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }
      6. obj3: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }


      可以看出$obj1和$obj3是同一个对象,而与$obj2则是不同的对象。违反了单例模式。


      考虑多线程


      [php]  ​​view plain​​​  ​​​copy​



      1. <?php
      2. require_once 'singleton.php';
      3. class Mythread extends Thread {
      4. public function __construct($i) {
      5. $this->i = $i;
      6. }
      7. public function run() {
      8. $obj = Singleton::getInstance();
      9. 'obj');
      10. }
      11. }
      12.
      13. for ( $i=1; $i<10; $i++) {
      14. $threads[$i]=new MyThread($i);
      15. $threads[$i]->start();
      16. }


      输出case



      [plain]  ​​view plain​​​  ​​​copy​


      1. Singleton 685692620930 I'm construct! process id is 27349 and thread id is 139824163313408
      2. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=685692620930 }
      3. Singleton 578721798491 I'm construct! process id is 27349 and thread id is 139824152233728
      4. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=578721798491 }
      5. Singleton 334907566198 I'm construct! process id is 27349 and thread id is 139824069605120
      6. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=334907566198 }
      7. Singleton 940285742749 I'm construct! process id is 27349 and thread id is 139824059115264
      8. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=940285742749 }
      9. Singleton 41907731444 I'm construct! process id is 27349 and thread id is 139824048625408
      10. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=41907731444 }
      11. Singleton 492959984113 I'm construct! process id is 27349 and thread id is 139824038135552
      12. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=492959984113 }
      13. Singleton 561926315539 I'm construct! process id is 27349 and thread id is 139824027645696
      14. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=561926315539 }
      15. Singleton 829729639926 I'm construct! process id is 27349 and thread id is 139824017155840
      16. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=829729639926 }
      17. Singleton 435530856252 I'm construct! process id is 27349 and thread id is 139823935387392
      18. obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=435530856252 }



      目前可以想到以上三种可以破坏上述单例模式的情形,下面针对上述三个方面,试着探讨一些相应的解决方案。

      针对反射

      设置标志位,第一次调用构造函数时开启标志位,第二次调用构造函数时抛出异常。


      [php]  ​​view plain​​​  ​​​copy​



      1. <?php
      2.
      3. class Singleton {
      4. //保存类实例的静态成员变量
      5. private static $_instance;
      6. private $_serialize_id = 1234567890;
      7. private static $_flag = false;
      8.
      9. //private 构造函数
      10. private function __construct() {
      11. if ( self::$_flag ) {
      12. throw new Exception("I'm Singleton");
      13. }
      14. else {
      15. $_flag = true;
      16. }
      17. $this->setSerializeId(rand(1,1000000000000));
      18. echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
      19. }
      20.
      21. private function __clone() {
      22. echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
      23. }
      24.
      25. /**
      26. * @return mixed
      27. */
      28. public function getSerializeId() {
      29. return $this->_serialize_id;
      30. }
      31.
      32. /**
      33. * @param mixed $serialize_id
      34. */
      35. public function setSerializeId($serialize_id) {
      36. $this->_serialize_id = $serialize_id;
      37. }
      38.
      39. //单例方法访问类实例
      40. public static function getInstance() {
      41. if (!(self::$_instance instanceof self)) {
      42. $_instance = new self();
      43. }
      44. return self::$_instance;
      45. }
      46. public function __toString()
      47. {
      48. return __CLASS__ . " " . $this->getSerializeId() ;
      49. }
      50. }

      针对序列化

      由于在序列化之前会试图调用__sleep()方法,相应的,在重新构造对象之后,会调用__wakeup()方法。与__clone()方法不同,序列化的时候__sleep()方法只是序列化动作之前调用,将其设置为private并不会起作用,只是运行的时候会收到一个notice。可以试着在__sleep()方法抛出异常的方式来阻止序列化的达成。不过使用这种方式,如果没有捕获异常,或者没有异常处理函数,将导致程序异常退出,并不是很完美。

      在Singleton类中增加__sleep()及__wakeup()方法,并执行测试case

      Singleton 594976518769 I'm construct! process id is 27612 and thread id is 139941710354368

      [plain]  ​​view plain​​​  ​​​copy​


      1. PHP Fatal error:  Uncaught exception 'Exception' with message 'Not allowed serizlization' in /data1/study/php/singleton.php:44
      2. Stack trace:
      3. #0 [internal function]: Singleton->__sleep()
      4. #1 /data1/study/php/test2.php(11): serialize(Object(Singleton))
      5. #2 {main}
      6. thrown in /data1/study/php/singleton.php on line 44
      7.
      8. Fatal error: Uncaught exception 'Exception' with message 'Not allowed serizlization' in /data1/study/php/singleton.php on line 44
      9.
      10. Exception: Not allowed serizlization in /data1/study/php/singleton.php on line 44
      11.
      12. Call Stack:
      13. 0.0007 227224 1. {main}() /data1/study/php/test2.php:0
      14. 0.0010 244080 2. serialize(???) /data1/study/php/test2.php:11
      15. 0.0010 244448 3. Singleton->__sleep() /data1/study/php/test2.php:11

      在这个测试case中,发现了另外一个问题,《​​php中$this的引用计数​​》


      针对多线程

      目前还没有想到针对多线程的解决方案。



      单例模式与trait结合,可以实现一个单例模式的模板,关于php中trait的使用参见《​​php中的trait​​》


      [php]  ​​view plain​​​  ​​​copy​



      1. <?php
      2.
      3. trait TSingleton {
      4. private $_serialize_id = 1234567890;
      5. private static $_flag = false ;
      6. //private 构造函数
      7. private function __construct() {
      8. if ( self::$_flag ) {
      9. throw new Exception("I'm a Singleton");
      10. }
      11. else {
      12. $_flag = true;
      13. }
      14. $this->setSerializeId(rand(1,1000000000000));
      15. echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
      16. }
      17. private function __clone() {
      18. echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
      19. }
      20.
      21. /**
      22. * @return mixed
      23. */
      24. public function getSerializeId() {
      25. return $this->_serialize_id;
      26. }
      27.
      28. /**
      29. * @param mixed $serialize_id
      30. */
      31. public function setSerializeId($serialize_id) {
      32. $this->_serialize_id = $serialize_id;
      33. }
      34.
      35. //单例方法访问类实例
      36. public static function getInstance() {
      37. static $instance ;
      38. if (!($instance instanceof self )) {
      39. $ref = new ReflectionClass( get_called_class() );
      40. $ctor = $ref->getConstructor();
      41. $ctor->setAccessible(true);
      42. $instance = $ref->newInstanceWithoutConstructor();
      43. $ctor->invokeArgs($instance, func_get_args());
      44. }
      45. return $instance;
      46. }
      47. public function __toString()
      48. {
      49. return __CLASS__ . " " . $this->getSerializeId() ;
      50. }
      51.
      52. public function __sleep()
      53. {
      54. // TODO: Implement __sleep() method.
      55. throw new Exception("I'm Singleton! Can't serialize");
      56. }
      57.
      58. public function __wakeup()
      59. {
      60. // TODO: Implement __wakeup() method.
      61. throw new Exception("I'm Singleton! Can't unserialize");
      62. }
      63. }
      64.
      65. class Singleton {
      66. use TSingleton;
      67. }