你真的懂单例模式么

本文首发于个人我的博客

什么是单例

在开发中,单例模式应该是每一个人都会用的,可是你真的深刻了解过单例模式么?但愿这篇文章能给你更加深刻的认识。html

wikipedia中这么介绍

单例模式,也叫单子模式,是一种经常使用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只须要拥有一个的全局对象,这样有利于咱们协调系统总体的行为。好比在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,而后服务进程中的其余对象再经过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。git

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个得到该实例的方法(必须是静态方法,一般使用getInstance这个名称);当咱们调用这个方法时,若是类持有的引用不为空就返回这个引用,若是类保持的引用为空就建立该类的实例并将实例的引用赋予该类保持的引用;同时咱们还将该类的构造函数定义为私有方法,这样其余处的代码就没法经过调用该类的构造函数来实例化该类的对象,只有经过该类提供的静态方法来获得该类的惟一实例。github

单例模式在多线程的应用场合下必须当心使用。若是当惟一实例还没有建立时,有两个线程同时调用建立方法,那么它们同时没有检测到惟一实例的存在,从而同时各自建立了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例惟一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会下降效率)。web

苹果官方定义

苹果官方示例中以下定义单例设计模式

static MyGizmoClass *sharedGizmoManager = nil;
 
+ (MyGizmoClass*)sharedManager
{
    if (sharedGizmoManager == nil) {
        sharedGizmoManager = [[super allocWithZone:NULL] init];
    }
    return sharedGizmoManager;
}
 
+ (id)allocWithZone:(NSZone *)zone
{
    return [[self sharedManager] retain];
}
 
- (id)copyWithZone:(NSZone *)zone
{
    return self;
}
 
- (id)retain
{
    return self;
}
 
- (NSUInteger)retainCount
{
    return NSUIntegerMax;  //denotes an object that cannot be released
}
 
- (void)release
{
    //do nothing
}
 
- (id)autorelease
{
    return self;
}

复制代码

问题:为何用了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:bash

>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.app

而实践证实,使用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已经被废弃了,只是由于历史缘由才保留了这个接口。因此咱们应该怎么使用单例呢?

现代单例模式实现

在前辈大牛的指引下,后人总能站的更高,看得更远

现代通常单例实现以下

+ (instancetype)sharedInstance
{
  static dispatch_once_t onceToken = 0;
  __strong static id _sharedObject = nil;
  dispatch_once(&onceToken, ^{
    _sharedObject = [[self alloc] init]; // or some other init method
  });
  return _sharedObject;
}

复制代码

dispatch_once

@synchronizeddispatch_once对比

咱们之因此使用dispatch_once 主要是由于为了加锁保证单例的惟一性,由于苹果官方推荐的allocWithZone已经被废弃了。那么问题来了,若是要加锁来保证单例的惟一性,也能够用@synchronized呀,为何用的是 dispatch_once,而不是@synchronized

国外有开发者作过性能测试@synchronized 和dispatch_once对比。在单线程和多线程状况下测试了 @synchronizeddispatch_once 实现单例的性能对比,结果以下:

Single threaded results
-----------------------
  @synchronized: 3.3829 seconds
  dispatch_once: 0.9891 seconds

Multi threaded results
----------------------
  @synchronized: 33.5171 seconds
  dispatch_once: 1.6648 seconds
复制代码

能够看到,dispatch_once 在线程竞争环境下性能显著优于 @synchronized

dispatch_once分析

Objective-C 中,@synchronized 是用 NSRecursiveLock 实现的,而且隐式添加一个 exception handler,若是有异常抛出,handler 会自动释放互斥锁。而 dispatch_once 之因此拥有高性能是由于它省去了锁操做,代替的是大量的原子操做,该原子操做内部不是靠 pthread 等锁来实现,而是直接利用了 lock 的汇编指令,靠底层 CPU 指令来支持的。

咱们以下代码

#import "YZPerson.h"

@implementation YZPerson
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken = 0;
    __strong static id _sharedObject = nil;
    NSLog(@"before dispatch_once onceToken = %ld",onceToken);
    dispatch_once(&onceToken, ^{
          NSLog(@"before dispatch_once onceToken = %ld",onceToken);
        _sharedObject = [[self alloc] init]; // or some other init method
    });
      NSLog(@"before dispatch_once onceToken = %ld",onceToken);
    return _sharedObject;
}
@end
复制代码

dispatch_once以前,进行中,以后,分别打印onceToken的值。

屡次调用单例

[YZPerson sharedInstance];
[YZPerson sharedInstance];
[YZPerson sharedInstance];
[YZPerson sharedInstance];
[YZPerson sharedInstance];
复制代码

输出结果为

iOS-单例模式[8255:91704] before dispatch_once onceToken = 0
iOS-单例模式[8255:91704] before dispatch_once onceToken = 772
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
iOS-单例模式[8255:91704] before dispatch_once onceToken = -1
复制代码
  • 经过输出咱们能够发现,在 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 的实现须要知足如下三种场景的需求:

  1. dispatch_once 第一次执行,block 被调用,调用结束需标记 onceToken
  2. dispatch_once 第一次执行过程当中,有其它线程执行该 dispatch_once,则其它线程的请求须要等待 dispatch_once 的第一次执行结束才能被处理。
  3. dispatch_once 第一次执行已经结束,有其它线程执行该 dispatch_once,则其它线程直接跳过 block 执行后续任务。

因为场景 1 只会发生一次,场景 2 发生的次数也是有限的,甚至根本不会发生,而场景 3 的发生次数多是很是高的数量级,也正是影响 dispatch_once 性能的关键所在。

对于场景三的优化:

OC中,dispatch_once的代码是开源的,咱们直接查看源码

#ifdef __BLOCKS__
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)  
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW  
void  
dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_NONNULL_ALL DISPATCH_NOTHROW  
void  
_dispatch_once(dispatch_once_t *predicate, dispatch_block_t block)  
{
    // 告诉 CPU *predicate 等于 ~0l 的可能性很是高,
    // 这就使得 CPU 预测不进入 if 分支,提早取后续指令,译码,
    // 甚至提早计算一些结果,提升效率,
    // 场景 3 的性能优化主要在此体现
    if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
        dispatch_once(predicate, block);
    }
}
#undef dispatch_once
#define dispatch_once _dispatch_once
#endif
复制代码

经过宏定义 #define dispatch_once _dispatch_once可知,咱们实际调用的是 _dispatch_once方法,而且是强制 inlineDISPATCH_EXPECT__builtin_expect((x), (v)) 的宏替换,long __builtin_expect (long EXP, long C) 是 GCC 提供的内建函数来处理分支预测,EXP 为一个整型表达式,这个内建函数的返回值也是 EXP,C 为一个编译期常量。这个函数至关于告诉编译器,EXP == C 的可能性很是高,其做用是帮助编译器判断条件跳转的预期值,编译器会产生相应的代码来优化 CPU 执行效率,CPU 遇到条件转移指令时会提早预测并装载某个分支的指令,避免跳转形成时间乱费,但并无改变其对真值的判断,若是分支预测错了,就会丢弃以前的指令,从正确的分支从新开始执行。

对于场景一,场景二的处理:

dispatch_once 的写入端来保证,实现以下:

struct _dispatch_once_waiter_s {  
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    // _dispatch_thread_semaphore_t 是 unsigned long 类型的别名,用来表示信号量
    _dispatch_thread_semaphore_t dow_sema;
};

// 将 DISPATCH_ONCE_DONE 定义为 _dispatch_once_waiter_s 类型的指针,
// ~0l 是 long 的 0 取反,也就是一大堆 1(输出为 -1),是个无效的指针,
// 即指向的地址不可能为一个有效的 _dispatch_once_waiter_s 类型,
// 用来标记 onceToken,表示 dispatch_once 第一次执行已经完成
#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)

#ifdef __BLOCKS__
void  
dispatch_once(dispatch_once_t *val, dispatch_block_t block)  
{
    // dispatch_block_t 的类型定义:typedef void (^dispatch_block_t)(void)
    struct Block_basic *bb = (void *)block;
    // 执行 block 最终是调用 C 函数
    dispatch_once_f(val, block, (void *)bb->Block_invoke);
}
#endif

// val 即外部传入的 &onceToken,ctxt 传入指向 block 的指针,可取到 block 上下文,
// dispatch_function_t 的类型定义:typedef void (*dispatch_function_t)(void *)
// func 是 block 内部的函数指针,指向函数执行体,执行它就是执行 block
DISPATCH_NOINLINE  
void  
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)  
{
    // volatile 是一个类型修饰符,用来修饰被不一样线程访问和修改的变量,
    // 遇到这个关键字声明的变量,编译器对访问该变量的代码就再也不进行优化,
    // 优化器在用到这个变量时必须从新从它所在的内存读取数据,而不是使用保存在寄存器里的备份
    struct _dispatch_once_waiter_s * volatile *vval =
            (struct _dispatch_once_waiter_s**)val;
    // dow 意为 dispatch_once waiter
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;

    // dispatch_atomic_cmpxchg 是原子比较交换函数 __sync_bool_compare_and_swap 的宏替换,
    // 原理是大体以下(真正的实现并不是如此):
    //     if(*vval == NULL)
    //     {
    //         *vval = &dow;
    //         return true;
    //     }
    //     else
    //     {
    //         return false;
    //     }
    // 当 dispatch_once 第一次执行时,*vval 为 0,
    // 则 *vval 被值赋值为 &dow 并返回 true,
    // 此时 *vval 的值是相似上文中的 140734723410256
    if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
        // 空的宏替换,什么都不作
        dispatch_atomic_acquire_barrier();
        // _dispatch_client_callout 实际上就是调用了func,执行了 block,即初始化并写入 obj
        _dispatch_client_callout(ctxt, func);

        dispatch_atomic_maximally_synchronizing_barrier();

        // dispatch_atomic_xchg 原子交换函数 __sync_swap 的宏替换,
        // 执行的操做是:
        //         temp = *vval;
        //         *vval = DISPATCH_ONCE_DONE;
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
        tail = &dow;
        // 若在 block 执行过程当中,没有其它线程进入线程等待分支来等待,
        // 则 *vval == &dow,即 tmp == &dow,while 循环不会被执行,分支结束,
        // 如有其它线程进入线程等待分支来等待,那么会构造一个信号量链表,
        // *vval 变为信号量链的头部,&dow 为链表的尾部,
        // 则在此 while 循环中,遍历链表来 signal 每一个信号量
         while (tail != tmp) {
            // 由于线程等待分支会中途将 val(即 *vval)赋值为 &dow,
            // 而后再为 val->dow_next 赋值,
            // 在 val->dow_next 赋值以前其值为 NULL,须要等待,
            // pause 就像 nop,延迟空等,主要是提升性能和节省 CPU 耗电
            while (!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    } else {
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for (;;) {
            tmp = *vval;
            // 若是发现 *vval 已经为 DISPATCH_ONCE_DONE,则直接break,
            // 而后调用 _dispatch_put_thread_semaphore 销毁信号量
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            // 空的宏替换,什么都不作
            dispatch_atomic_store_barrier();
            // 若是 *vval 不为 DISPATCH_ONCE_DONE,则进行原子比较并交换操做,
            // 若是期间有其它线程同时进入线程等待分支并交错修改链表,则可能致使 *vval != tmp,
            // 则 for 循环从新开始,从新获取一次 vval 来进行一样的操做,
            // 若 *vval == tmp,则将 *vval 赋值为 &dow,
            // 接着执行 dow.dow_next = tmp 增长链表节点,而后等待信号量,
            // 当 block 执行分支完成并遍历链表来 signal 时,结束等待往下执行
            if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
             dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}
复制代码

因为 CPU 的流水线特性,有一种边缘情况可能出现。假如线程 a 在初始化并写入 obj 还没有完成时,线程 b 读取了 obj,则此时 obj 为 nil,而线程 b 在线程 a 置 predicateDISPATCH_ONCE_DONE 以后读取 predicate,线程 b 会认为 obj 初始化已经完成,将空的 obj 返回,那么接下来关于 obj 函数调用可能会致使程序崩溃。

假如写入端能在 初始化并写入 obj 与 置 predicateDISPATCH_ONCE_DONE 之间等待足够长的时间,即知足 Ta > Tb,那上述的问题就都解决了。所以 dispatch_once 在执行了 block 以后,会调用 dispatch_atomic_maximally_synchronizing_barrier() 宏函数,在 intel 处理器上,这个函数编译出的是 cpuid 指令,并强制将指令流串行化,在其余厂商处理器上,这个宏函数编译出的是合适的其它指令,这些指令都将耗费可观数量的 CPU 时钟周期,以保证 Ta > Tb。

总结,为了性能的优化,dispatch_once作到了极致

宏定义

前面说了这么多单例,实际使用的时候,咱们能够用宏来定义,之后只须要一行就能够了。

#define SYNTHESIZE_SINGLETON_FOR_CLASS_HEADER(className) \
\
+ (className *)sharedInstance;

#define SYNTHESIZE_SINGLETON_FOR_CLASS(className) \
\
+ (className *)sharedInstance { \
static className *sharedInstance = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
sharedInstance = [[self alloc] init]; \
}); \
return sharedInstance; \
}
复制代码

使用的时候

//YZPerson类单例的声明
SYNTHESIZE_SINGLETON_FOR_CLASS_HEADER(YZPerson)
// YZPerson类单例的实现
SYNTHESIZE_SINGLETON_FOR_CLASS(YZPerson)
复制代码

参考资料:

苹果关于allocWithZone的文档

GCD 中 dispatch_once 的性能与实现

从 Objective-C 里的 Alloc 和 AllocWithZone 谈起

@synchronized 和dispatch_once对比

更多资料,欢迎关注我的公众号,不定时分享各类技术文章。

相关文章
相关标签/搜索