php如何實現單例模式
凡是講到設計模式,無一例外的都會講到單例模式,單例模式相對於其他設計模式來講,要容易理解的多,但是要實現一個嚴格意義上的單例模式,很簡單嗎?
很多人可以輕鬆的寫出如下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;
}