单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
意图
单例模式(Singleton)是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
结构
使用下三部分来实现:
- 私有构造函数:私有构造函数保证了不能通过构造函数来创建对象实例
- 一个私有静态变量:私有静态变量用于指向实例化的对象,全局唯一
- 一个公有静态函数:只能通过公有静态函数返回唯一的私有静态变量
实现
主要分为懒汉式 和 饿汉式 两种实现方式。区别就是实例化唯一的对象的位置。而懒汉式又有两种实现,一个线程安全,一个线程不安全
- 懒汉式:延迟实现,只有当调用公用静态函数,且静态指针为空的时候才实例化对象,存在线程不安全问题
- 线程安全的修改
- 饿汉式:当程序运行的时候就 初始化 私有静态指针,因此线程安全
C++ 代码实现
饿汉式 / 懒汉式
1 | class Singleton |
同过上述代码可以看出,懒汉式饿汉式的区别:
- 饿汉式:在 main 函数的执行之前,就调用了 new 实例化了对象,因此是线程安全的;缺点是由于一开始就申请了空间,会有资源浪费的问题。
- 懒汉式:只有在程序 调用 公有静态函数,需要获取对象时,才会判断该对象有没有实例化,没的话再实例化。优点是 延迟实例化带来的节约资源的好处。缺点是 这是线程不安全的。
- 例如当两个线程同时 调用
GetInstance
函数,并且都同时通过了singleton_
指针空的判断,就会实例化出两个对象,所以上述代码输出的现象就是,两个线程输出的value值不一样。
- 例如当两个线程同时 调用
懒汉式-线程安全
为了解决上述 懒汉式 线程不安全的问题就是 对 GetInstance
函数加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次。但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 Singleton
已经被实例化了(即 singleton_
指针不为空)。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。可以使用 双重校验锁 的方法:
1 | // 简单的加锁 |
从上述代码实现可以看出,双重校验锁的原理是,只有当判断 singleton_
为空,即还未实例化的时候,才需要加锁操作,因为这时候肯定是要进行对象实例化操作,即写操作。如果已经实例化了,直接返回地址即可,不用加锁读。但是加锁后又判断了一遍是否指针空,因为还是可能存在两个线程同时进入了第一个 指针判断 空的语句,由于有锁,一个线程实例化对象,并修改指针为非空,完毕后释放锁。此时如果另一个线程不再判断一次为空就会 再次 new 实例化,还是存在线程不安全的问题。
或者直接这么理解,双重校验就是在 原本的基础上 先判断指针是否为空,空的化直接返回指针,不必校验。而不为空,才加锁判断。
优点:双重校验锁,即是延迟实例化的,可以节省资源,还是线程安全的,且不会因为锁的存在使得线程阻塞时间过长。
volatile 关键字
singleton_ 采用 volatile 关键字修饰也是很有必要的,singleton_ = new Singleton(value)
这段代码其实是分为三步执行:
- 为
Singleton
分配内存空间 - 初始化
Singleton
- 将
singleton_
指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行