本文首发于我的个人博客
什么是单例
在开发中,单例模式应该是每个人都会用的,但是你真的深入了解过单例模式么?希望这篇文章能给你更加深入的认识。
wikipedia中这么介绍
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
苹果官方定义
苹果官方示例中如下定义单例
1 | static MyGizmoClass *sharedGizmoManager = nil; |
问题:为什么用了allocWithZone
官方文档描述
- 官方文档对于allocWithZone 的描述是
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
You must use an init… method to complete the initialization process. For example:
1 | >TheClass *newObject = [[TheClass allocWithZone:nil] init]; |
Do not override allocWithZone: to include any initialization code. Instead, class-specific versions of init… methods.
This method exists for historical reasons; memory zones are no longer used by Objective-C.
文档提到,使用allocWithZone
是因为保证分配对象的唯一性
原因是单例类只有一个唯一的实例,而平时我们在初始化一个对象的时候, [[Class alloc] init]
,其实是做了两件事。 alloc
给对象分配内存空间,init
是对对象的初始化,包括设置成员变量初值这些工作。而给对象分配空间,除了alloc方法之外,还有另一个方法: allocWithZone
.
而实践证明,使用alloc
方法初始化一个类的实例的时候,默认是调用了 allocWithZone
的方法。于是覆盖allocWithZone
方法的原因已经很明显了:为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果有人初始化这个单例类的时候不走[[Class alloc] init]
,而是直接 allocWithZone
, 那么这个单例就不再是单例了,所以必须把这个方法也堵上。
allocWithZone
已经被废弃了
This method exists for historical reasons; memory zones are no longer used by Objective-C
前面说了 allocWithZone
是为了保证单例的唯一性,然而,文档中又说了allocWithZone
已经被废弃了,只是因为历史原因才保留了这个接口。所以我们应该怎么使用单例呢?
现代单例模式实现
在前辈大牛的指引下,后人总能站的更高,看得更远
现代一般单例实现如下
1 |
|
dispatch_once
@synchronized
和dispatch_once
对比
我们之所以使用dispatch_once
主要是因为为了加锁保证单例的唯一性,因为苹果官方推荐的allocWithZone
已经被废弃了。那么问题来了,如果要加锁来保证单例的唯一性,也可以用@synchronized
呀,为什么用的是 dispatch_once
,而不是@synchronized
呢
国外有开发者做过性能测试@synchronized 和dispatch_once对比。在单线程和多线程情况下测试了 @synchronized
与 dispatch_once
实现单例的性能对比,结果如下:
1 | Single threaded results |
可以看到,dispatch_once
在线程竞争环境下性能显著优于 @synchronized
。
dispatch_once
分析
在 Objective-C
中,@synchronized
是用 NSRecursiveLock
实现的,并且隐式添加一个 exception handler
,如果有异常抛出,handler
会自动释放互斥锁。而 dispatch_once
之所以拥有高性能是因为它省去了锁操作,代替的是大量的原子操作,该原子操作内部不是靠 pthread
等锁来实现,而是直接利用了 lock
的汇编指令,靠底层 CPU 指令来支持的。
我们如下代码
1 |
|
在dispatch_once
之前,进行中,之后,分别打印onceToken
的值。
多次调用单例
1 | [YZPerson sharedInstance]; |
输出结果为
1 | iOS-单例模式[8255:91704] before dispatch_once onceToken = 0 |
- 通过输出我们可以发现,在
dispatch_once
执行前,onceToken
的值是 0,因为dispatch_once_t
是由typedef long dispatch_once_t
而来,所以在onceToken
还没被手动赋值的情况下,0 是编译器给onceToken
的初始化赋值。 - 在
dispatch_once
执行过程中,onceToken
是一个很大的数字,这个值是dispath_once
内部实现中一个局部变量的地址,并不是一个固定的值。 - 当
dispatch_once
执行完毕,onceToken
的值被赋为 -1。之后再次调用的时候,onceToken
已经是-1了,就直接跳过dispatch_once
的执行
dispatch_once
使用场景
所以 dispatch_once
的实现需要满足以下三种场景的需求:
dispatch_once
第一次执行,block
被调用,调用结束需标记onceToken
。dispatch_once
第一次执行过程中,有其它线程执行该dispatch_once
,则其它线程的请求需要等待dispatch_once
的第一次执行结束才能被处理。dispatch_once
第一次执行已经结束,有其它线程执行该dispatch_once
,则其它线程直接跳过 block 执行后续任务。
由于场景 1 只会发生一次,场景 2 发生的次数也是有限的,甚至根本不会发生,而场景 3 的发生次数可能是非常高的数量级,也正是影响 dispatch_once
性能的关键所在。
对于场景三的优化:
OC中,dispatch_once
的代码是开源的,我们直接查看源码
1 | #ifdef __BLOCKS__ |
通过宏定义 #define dispatch_once _dispatch_once
可知,我们实际调用的是 _dispatch_once
方法,并且是强制 inline
。DISPATCH_EXPECT
是 __builtin_expect((x), (v))
的宏替换,long __builtin_expect (long EXP, long C)
是 GCC 提供的内建函数来处理分支预测,EXP 为一个整型表达式,这个内建函数的返回值也是 EXP,C 为一个编译期常量。这个函数相当于告诉编译器,EXP == C 的可能性非常高,其作用是帮助编译器判断条件跳转的预期值,编译器会产生相应的代码来优化 CPU 执行效率,CPU 遇到条件转移指令时会提前预测并装载某个分支的指令,避免跳转造成时间乱费,但并没有改变其对真值的判断,如果分支预测错了,就会丢弃之前的指令,从正确的分支重新开始执行。
对于场景一,场景二的处理:
在 dispatch_once
的写入端来保证,实现如下:
1 | struct _dispatch_once_waiter_s { |
由于 CPU 的流水线特性,有一种边缘状况可能出现。假如线程 a 在初始化并写入 obj 尚未完成时,线程 b 读取了 obj,则此时 obj 为 nil,而线程 b 在线程 a 置 predicate
为 DISPATCH_ONCE_DONE
之后读取 predicate
,线程 b 会认为 obj 初始化已经完成,将空的 obj 返回,那么接下来关于 obj 函数调用可能会导致程序崩溃。
假如写入端能在 初始化并写入 obj 与 置 predicate
为 DISPATCH_ONCE_DONE
之间等待足够长的时间,即满足 Ta > Tb,那上述的问题就都解决了。因此 dispatch_once
在执行了 block 之后,会调用 dispatch_atomic_maximally_synchronizing_barrier()
宏函数,在 intel 处理器上,这个函数编译出的是 cpuid 指令,并强制将指令流串行化,在其他厂商处理器上,这个宏函数编译出的是合适的其它指令,这些指令都将耗费可观数量的 CPU 时钟周期,以保证 Ta > Tb。
总结,为了性能的优化,dispatch_once
做到了极致
宏定义
前面说了这么多单例,实际使用的时候,我们可以用宏来定义,以后只需要一行就可以了。
1 | #define SYNTHESIZE_SINGLETON_FOR_CLASS_HEADER(className) \ |
使用的时候
1 | //YZPerson类单例的声明 |
参考资料:
从 Objective-C 里的 Alloc 和 AllocWithZone 谈起
@synchronized 和dispatch_once对比
更多资料,欢迎关注个人公众号,不定时分享各种技术文章。