凡是讲到设计模式,无一例外的都会讲到单例模式,单例模式相对于其他设计模式来讲,要容易理解的多,但是要实现一个严格意义上的单例模式,很简单吗?
很多人可以轻松的写出如下php实现的单例模式:
<?php
class Singleton {
//保存类实例的静态成员变量
private static $_instance;
//private 构造函数
private function __construct() {
echo " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
private function __clone() {
echo " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
//单例方法访问类实例
public static function getInstance() {
if (!(self::$_instance instanceof self)) {
self::$_instance = new self();
}
return self::$_instance;
}
}
在该示例中,将构造方法设为private从而防止直接new一个对象;将__clone方法设为private,防止通过clone复制一个对象;需要该类对象"只能"通过调用Singleton::getInstance()方法的方式,而getInstance方法通过"饿汉模式"保证类变量$_instance只会被初始化一次,即Singleton类只能有一个对象。
这种实现方式看似没有问题,当我们试图 new Singleton()或者clone 一个对象时都会发生fatal error。那么,这种方式是否就能保证单例了?并不是。
考虑反射
构造方法被private了,是不是就无法实例化一个类了?来看ReflectionClass的一个方法
ReflectionClass::newInstanceWithoutConstructor — 创建一个新的类实例而不调用它的构造函数
也就是通过这个方法可以不经过构造方法就创建一个对象,上例中试图将构造方法private来阻止实例对象的方法失效了。下面来验证可行性。
为了方便验证,会在上例中加入一些属性及方法。
<?php
class Singleton {
//保存类实例的静态成员变量
private static $_instance;
private $_serialize_id = 1234567890;
//private 构造函数
private function __construct() {
$this->setSerializeId(rand(1,1000000000000));
echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
private function __clone() {
echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
/**
* @return mixed
*/
public function getSerializeId() {
return $this->_serialize_id;
}
/**
* @param mixed $serialize_id
*/
public function setSerializeId($serialize_id) {
$this->_serialize_id = $serialize_id;
}
//单例方法访问类实例
public static function getInstance() {
if (!(self::$_instance instanceof self)) {
self::$_instance = new self();
}
return self::$_instance;
}
public function __toString()
{
return __CLASS__ . " " . $this->getSerializeId() ;
}
}
测试用例脚本:
<?php
require_once 'singleton.php';
//$obj1 and $obj3 is the same object
$obj1 = Singleton::getInstance();
$obj3 = Singleton::getInstance();
//$obj2 is a new object
$class = new ReflectionClass('Singleton');
$obj2 = $class->newInstanceWithoutConstructor();
$ctor = $class->getConstructor();
$ctor->setAccessible(true);
$ctor->invoke($obj2);
echo "obj1 equal to obj3: " . ($obj1 === $obj3) . "\n";
echo "obj1 not equal obj2: " . ($obj1 !== $obj2) . "\n";
xdebug_debug_zval('obj1');
xdebug_debug_zval('obj2');
xdebug_debug_zval('obj3');
输出case:
Singleton 840562594589 I'm construct! process id is 30019 and thread id is 140410609465280
Singleton 920373440721 I'm construct! process id is 30019 and thread id is 140410609465280
obj1 equal to obj3: 1
obj1 not equal obj2: 1
obj1: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=840562594589 }
obj2: (refcount=1, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=920373440721 }
obj3: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=840562594589 }
可以看出$obj1和$obj3是同一个对象,而与$obj2则是不同的对象。违反了单例模式。
考虑序列化
<?php
require_once 'singleton.php';
//$obj1 and $obj3 is the same object
$obj1 = Singleton::getInstance();
$obj3 = Singleton::getInstance();
//$obj2 is a new object
$objSer = serialize($obj1);
$obj2 = unserialize($objSer);
echo "obj1 equal to obj3: " . ($obj1 === $obj3) . "\n";
echo "obj1 not equal obj2: " . ($obj1 !== $obj2) . "\n";
xdebug_debug_zval('obj1');
xdebug_debug_zval('obj2');
xdebug_debug_zval('obj3');
输出case:
Singleton 165926147718 I'm construct! process id is 6849 and thread id is 139844633716672
obj1 equal to obj3: 1
obj1 not equal obj2: 1
obj1: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }
obj2: (refcount=1, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }
obj3: (refcount=3, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=165926147718 }
可以看出$obj1和$obj3是同一个对象,而与$obj2则是不同的对象。违反了单例模式。
考虑多线程
<?php
require_once 'singleton.php';
class Mythread extends Thread {
public function __construct($i) {
$this->i = $i;
}
public function run() {
$obj = Singleton::getInstance();
xdebug_debug_zval('obj');
}
}
for ( $i=1; $i<10; $i++) {
$threads[$i]=new MyThread($i);
$threads[$i]->start();
}
输出case
Singleton 685692620930 I'm construct! process id is 27349 and thread id is 139824163313408
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=685692620930 }
Singleton 578721798491 I'm construct! process id is 27349 and thread id is 139824152233728
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=578721798491 }
Singleton 334907566198 I'm construct! process id is 27349 and thread id is 139824069605120
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=334907566198 }
Singleton 940285742749 I'm construct! process id is 27349 and thread id is 139824059115264
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=940285742749 }
Singleton 41907731444 I'm construct! process id is 27349 and thread id is 139824048625408
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=41907731444 }
Singleton 492959984113 I'm construct! process id is 27349 and thread id is 139824038135552
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=492959984113 }
Singleton 561926315539 I'm construct! process id is 27349 and thread id is 139824027645696
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=561926315539 }
Singleton 829729639926 I'm construct! process id is 27349 and thread id is 139824017155840
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=829729639926 }
Singleton 435530856252 I'm construct! process id is 27349 and thread id is 139823935387392
obj: (refcount=2, is_ref=0)=class Singleton { private $_serialize_id = (refcount=1, is_ref=0)=435530856252 }
目前可以想到以上三种可以破坏上述单例模式的情形,下面针对上述三个方面,试着探讨一些相应的解决方案。
针对反射
设置标志位,第一次调用构造函数时开启标志位,第二次调用构造函数时抛出异常。
<?php
class Singleton {
//保存类实例的静态成员变量
private static $_instance;
private $_serialize_id = 1234567890;
private static $_flag = false;
//private 构造函数
private function __construct() {
if ( self::$_flag ) {
throw new Exception("I'm Singleton");
}
else {
self::$_flag = true;
}
$this->setSerializeId(rand(1,1000000000000));
echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
private function __clone() {
echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
/**
* @return mixed
*/
public function getSerializeId() {
return $this->_serialize_id;
}
/**
* @param mixed $serialize_id
*/
public function setSerializeId($serialize_id) {
$this->_serialize_id = $serialize_id;
}
//单例方法访问类实例
public static function getInstance() {
if (!(self::$_instance instanceof self)) {
self::$_instance = new self();
}
return self::$_instance;
}
public function __toString()
{
return __CLASS__ . " " . $this->getSerializeId() ;
}
}
针对序列化
由于在序列化之前会试图调用__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
PHP Fatal error: Uncaught exception 'Exception' with message 'Not allowed serizlization' in /data1/study/php/singleton.php:44
Stack trace:
#0 [internal function]: Singleton->__sleep()
#1 /data1/study/php/test2.php(11): serialize(Object(Singleton))
#2 {main}
thrown in /data1/study/php/singleton.php on line 44
Fatal error: Uncaught exception 'Exception' with message 'Not allowed serizlization' in /data1/study/php/singleton.php on line 44
Exception: Not allowed serizlization in /data1/study/php/singleton.php on line 44
Call Stack:
0.0007 227224 1. {main}() /data1/study/php/test2.php:0
0.0010 244080 2. serialize(???) /data1/study/php/test2.php:11
0.0010 244448 3. Singleton->__sleep() /data1/study/php/test2.php:11
在这个测试case中,发现了另外一个问题,《php中$this的引用计数》
针对多线程
目前还没有想到针对多线程的解决方案。
单例模式与trait结合,可以实现一个单例模式的模板,关于php中trait的使用参见《php中的trait》
<?php
trait TSingleton {
private $_serialize_id = 1234567890;
private static $_flag = false ;
//private 构造函数
private function __construct() {
if ( self::$_flag ) {
throw new Exception("I'm a Singleton");
}
else {
self::$_flag = true;
}
$this->setSerializeId(rand(1,1000000000000));
echo $this . " I'm construct! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
private function __clone() {
echo $this . " I'm clone! process id is " . getmypid() . " and thread id is " . Thread::getCurrentThreadId() . "\n";
}
/**
* @return mixed
*/
public function getSerializeId() {
return $this->_serialize_id;
}
/**
* @param mixed $serialize_id
*/
public function setSerializeId($serialize_id) {
$this->_serialize_id = $serialize_id;
}
//单例方法访问类实例
public static function getInstance() {
static $instance ;
if (!($instance instanceof self )) {
$ref = new ReflectionClass( get_called_class() );
$ctor = $ref->getConstructor();
$ctor->setAccessible(true);
$instance = $ref->newInstanceWithoutConstructor();
$ctor->invokeArgs($instance, func_get_args());
}
return $instance;
}
public function __toString()
{
return __CLASS__ . " " . $this->getSerializeId() ;
}
public function __sleep()
{
// TODO: Implement __sleep() method.
throw new Exception("I'm Singleton! Can't serialize");
}
public function __wakeup()
{
// TODO: Implement __wakeup() method.
throw new Exception("I'm Singleton! Can't unserialize");
}
}
class Singleton {
use TSingleton;
}