单例设计模式(Singleton Design Pattern)的意思是:一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例模式可以保证类的对象全局唯一。
- 场景一:处理资源访问冲突。例如:多线程的日志写入。
- 场景二:表示全局唯一类。从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。再比如,唯一递增 ID 号码生成器。
要实现一个单例,需要关注的点有下面几个:
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
- 考虑对象创建时的线程安全问题;
- 考虑是否支持延迟加载;
- 考虑 getInstance() 性能是否高(是否加锁)。
单例模式又分为饿汉式单例模式和懒汉式单例模式:
- 饿汉式单例模式:在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载实例。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
- 懒汉式单例模式:相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。
接下来使用PHP代码一步一步分析单例模式的实现。
/**
* 普通的可以调用的类
* 先定义一个类,实例化两次,看看是否全等(当两个对象是一个的时候才会全等)
*/
class ObjectA {
}
$objA1 = new ObjectA();
$objA2 = new ObjectA();
var_dump($objA1 === $objA2); //bool(false)
上面定义了一个普通的类,实例化两次,比较是否全等,输出false。如果我们要实现实例化两次(或者更多次)后的对象实例全等,就要用到单例模式了,改造如下:
class ObjectB {
private static $instance = null;
public static function getInstance() {
if (self::$instance === null) {
//把自身对象赋给一个自己的静态属性
self::$instance = new self();
}
return self::$instance;
}
//私有化构造函数,禁止直接new操作
private function __construct() {
}
}
$objB1 = ObjectB::getInstance();
$objB2 = ObjectB::getInstance();
var_dump($objB1 === $objB2); //bool(true)
注意上面对构造函数私有化。此时,实例化两次之后发现它们是全等的。但是还是存在一个问题,看下面的代码:
$objB3 = clone $objB1;
var_dump($objB1 === $objB3); //bool(false)
对其中一个实例进行克隆之后,判断全等还是false,因此还需要对单例类禁止克隆。改造如下:
class ObjectC {
private static $instance = null;
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
//私有化构造函数
private function __construct() {
}
//禁止克隆
private function __clone() {
}
}
$objC1 = ObjectC::getInstance();
$objC2 = ObjectC::getInstance();
var_dump($objC1 === $objC2); //bool(true)
$objC3 = clone $objC1; //此处报错:Fatal error: Uncaught Error: Call to private ObjectC::__clone() from context ''
var_dump($objC1 === $objC3); //程序不会执行到这里
因此,上面的代码就实现了一个单例模式。
深入分析单例模式:
“一个类只允许创建唯一一个对象”,那对象的唯一性的作用范围是什么呢? 是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象? 答案是后者,也就是说,单例模式创建的对象是进程唯一的。
如何实现线程唯一的单例? 我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。在Java语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。
如何实现集群环境下的单例? 我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
参考源代码:https://gitee.com/rxbook/php_design_pattern/blob/master/code05_Singleton.php