1. 程式人生 > >php如何實現單例模式

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;
}