细说@synchronized和dispatch_once[细读][笔记]

原文出处: 树下的老男孩   

http://ios.jobbole.com/87349/

工欲善其事,必先利其器

通常我们在实现单例时候都会使用synchronized或者dispatch_once方法,初始化往往是下面的样子:
使用synchronized方法实现:

使用dispatch_once方法实现:

性能差异

上面的这些写法大家应该都很熟悉,既然两种方式都能实现,我们来看看两者的性能差异,这里简单写了个测试的demo,使用两个方法分单线程跟多线程(采用dispatch_apply方式,性能相对较高)去访问一个单例对象一百万次,对比这期间的耗时,从iPod跟5s测试得到如下的结果

可以发现dispatch_once方法的性能要明显优于synchronized方法(多线程不采用dispathc_apply方式差距更明显),所以在实际的应用中我们可以多采用dispatch_once方式来实现单例。通常使用的时候了解这些就够了,不过想知道两者的具体差异就需要我们再迈进一步。

深入@synchronized(object)

翻看苹果的文档可以发现 @synchronized指令内部使用锁来实现多线程的安全访问,并且隐式添加了一个异常处理的handler,当异常发生时会自动释放锁。stackoverflow上看到@synchronized指令其实可以转换成objc_sync_enter跟objc_sync_exit,可以在头文件中找到这两个函数:

根据注释文档,objc_sync_enter会根据需要给每个传进来的对象创建一个互斥锁并lock,然后objc_sync_exit的时候unlock,这样就可以通过这个锁来实现多线程的安全访问,所以结合苹果文档可以认为

等价于

庆幸的是苹果已经将objc-runtime这部分开源,所以我们可以更进一步了解内部的实现,源码在这里,有兴趣也可以自己去查阅,这里简单介绍一下。
让我们先来看看几个数据结构,其中有些涉及到缓存,我们就不去考虑了:

首先看看SyncData这个数据结构,包含一个指向object的指针,这个object对象就是我们@synchronized时传进来的对象,也包含一个跟object关联的递归互斥锁recursive_mutex_t,该锁用来互斥访问object对象;同时还包含一个指向下一个SyncData的指针nextData,可以看出SyncData是一个链表中的节点;至于threadCount,这个值标示有几个线程正在访问这个对象,当threadCount==0的时候,会重用该SyncData对象,这是为了节省内存。
接下来看看SyncList,SyncList其实就是一个链表,data指向第一个SyncData节点,lock则是为了多线程安全访问该链表。
最后看下sDataLists静态哈希表对象,它以obj的指针为key,对应的value为SyncList链表。
了解上面之后,我们就可以看看objc_sync_enter跟objc_sync_exit的具体实现(摘取部分代码)

简单来说,调用objc_sync_enter(obj)时,会根据obj指针在哈希表sDataLists对应的链表SyncList,然后在链表中查询对应obj的SyncData对象,如果查询不到则创建一个新的SyncData对象(包含创建跟obj相关的递归互斥锁)并添加到链表中,然后使用SyncData对象上锁;调用objc_sync_exit(obj)时,使用SyncData对象解锁,因此通过这个锁便可确保@synchronized之间的代码线程安全。

11458529-1c42de5d32202095

sDataLists
深入dispatch_once

探讨了synchronized之后,我们再来说说dispatch_once。

根据官方文档,dispatch_once可以用来初始化一些全局的数据,它能够确保block代码在app的生命周期内仅被运行一次,而且还是线程安全的,不需要额外加锁;predicate必须指向一个全局或者静态的变量,不过使用predicate的话结果是未定义的,不过predicate有啥作用,如何实现block在整个生命周期执行一次?那我们只能从源码查找(源码地址:once)。
不过在这之前先简要介绍一下:

  • bool __sync_bool_compare_and_swap (type ptr, type oldval type newval, …)
    提供原子的比较和交换操作,如果当前值
    ptr == oldval,就将newval写入*ptr,当比较赋值操作成功后返回true
  • *__sync_synchronize (…)
    调用这个函数会产生一个full memory barrier ,用于保证CPU按照我们代码编写的顺序来执行代码,比如:
  • type __sync_swap(type *ptr, type value, …)
    提供原子交换操作的函数,交换第一个跟第二个参数的值,然后返回交换前第一个参数的旧值。
  • _dispatch_hardware_pause()
    调用这个函数主要是暗示处理器不要做额外的优化处理等,提高性能,节省CPU时间,可以查看这里了解更多
      • 信号量
        信号量是一个非负整数,定义了两种原子操作:wait跟signal来进行访,信号量主要用于线程同步。当一个线程调用wait操作时,如果信号量的值大于0,则获得资源并将信号量值减一,如果等于0线程睡眠直到信号量值大于0或者超时;singal将信号量的值加1,如果这时候有正在等待的线程,唤醒该线程。

      接下来看看具体代码,当我们调用dispatch_once时候,内部是调用dispatch_once_f函数,其中val就是外部传入的predicate值,ctxt为Block的指针,func则是Block内部具体实现的函数指针,由于源码比较短,所以我直接把源码贴出来(为了方便查看,有些不使用宏定义)。

      让我们来看看dispatch_once是如何确保block只执行一次。简单来说,当线程A在调用执行block并设置predicate为DISPATCH_ONCE_DONE(~0l)期间,如果有其他线程也在调用disptach_once,则这些线程会等待,各线程对应的信号量会加入到信号量链表中,等predicate设置为DISPATCH_ONCE_DONE后,也就是block执行完了,会根据信号量链表唤醒各个线程使其继续执行。

      12458529-2d1119321803ee9b

      信号量链表.png

      不过有一种临界情况,假如线程A在执行block,但是创建单例对象obj还未完成,这时候线程B获取该obj对象,此时obj=nil,而线程B在线程A将predicate设为DISPATCH_ONCE_DONE之后读取predicate,这是线程B会认为单例对象已经初始化完成,然后使用空的obj对象,这就会导致错误发生。因此dispatch_once会在执行完block之后会执行dispatch_atomic_maximally_synchronizing_barrier()调用,这个调用会执行一些cpuid指令,确保线程A创建单例对象obj以及置predicate为DISPATCH_ONCE_DONE的时间TimeA大于线程B进入block并读取predicate值的时间TimeB。

      除此之外,每次调用dispatch_once的时候,都会先判断predicate的值是否是~0l(也就是DISPATCH_ONCE_DONE),如果是则意味着block已经执行过了,便不再执行,代码如下:

      让我们看看这里面的__builtin_expect((x), (v)),这又是一个优化的地方。。。

      由于dispatch_once的只执行block一次,所以我们更期望的是已经block已经执行完了,也就是predict的值为~0l的可能性更大。
      现在我们清楚dispatch_once是如何确保block只执行一次了,关键就在predict这个值,通过比较这个值等于0或者~0l来判断block是否执行过,这也就是为啥我们需要将这个值设为static或者全局的缘故,因为各个线程都要去访问这个predict,有兴趣的可以试试把predicate的初始值设为非0或者非静态全局变量会发生什么~~

      总结

      通过上面的分析,我们知道

      参考

      objc-sync
      synchronized
      dispatch_once
      Built-in functions for atomic memory access
      __builtin_expect

    • @synchronized采用的是递归互斥锁来实现线程安全,而dispatch_once的内部则使用了很多原子操作来替代锁,以及通过信号量来实现线程同步,而且有很多针对处理器优化的地方,甚至在if判断语句上也做了优化(逼格有点高),使得其效率有很大的提升,虽然其源码很短,但里面包含的东西却很多,所以苹果也推荐使用dispatch_once来创建单例。通过这个简短的dispatch_once,你也可以清楚为什么GCD的性能会这么高了,感兴趣可以再去看看libdispatch的其它源码。。

      <think>我们正在讨论JavaObjective-C中的同步机制。 问题:方法声明上加synchronized(Java)使用@synchronized(Objective-C)有什么区别? 首先,需要明确这两种语法分别属于不同的语言: 1. 在Java中,我们可以在方法声明上使用`synchronized`关键字来实现同步。 2. 在Objective-C中,我们使用`@synchronized`指令来同步一段代码块。 接下来,我们将分别解释这两种机制,然后比较它们的区别。 一、Java中的方法级`synchronized`: 在Java中,将`synchronized`关键字放在实例方法上,意味着整个方法体被同步,并且锁对象是当前实例(即`this`)。 如果放在静态方法上,则锁对象是当前类的Class对象。 示例: ```java public class Counter { private int count = 0; // 实例方法同步,锁是当前实例 public synchronized void increment() { count++; } // 静态方法同步,锁是Counter.class public static synchronized void staticMethod() { // ... } } ``` 二、Objective-C中的`@synchronized`: 在Objective-C中,`@synchronized`是一个指令,它后面跟着一个对象作为锁(通常使用`self`),然后是一段代码块。这个指令会确保同时只有一个线程能够执行该代码块。 示例: ```objectivec @implementation Counter { NSInteger _count; } - (void)increment { @synchronized (self) { // 使用self作为锁对象 _count++; } } @end ``` 三、主要区别: 1. **语法结构**: - Java:作为方法修饰符,直接修饰整个方法。 - Objective-C:是一个代码块指令,需要显式指定锁对象,并包裹需要同步的代码块。 2. **锁对象**: - Java:在实例方法上,锁是当前的实例对象(`this`);在静态方法上,锁是当前类的Class对象。 - Objective-C:锁对象由程序员显式指定(通常为`self`或一个共享的锁对象),可以是任意Objective-C对象。 3. **粒度控制**: - Java:整个方法体都被同步,无法选择部分代码。 - Objective-C:可以精确控制需要同步的代码段,避免过度同步。 4. **异常处理**: - Java:如果同步方法中发生异常,锁会自动释放。 - Objective-C:如果`@synchronized`块内发生异常,锁也会自动释放,这是由Objective-C的异常处理机制保证的。 5. **性能**: - 在Java中,方法级的同步通常比同步块(`synchronized (obj) { ... }`)更简单,但可能因为同步范围过大而降低并发性能。 - 在Objective-C中,`@synchronized`因为需要动态创建异常处理结构,所以性能相对较低,但提供了更细粒度的控制。 四、Java中对应的代码块同步(对比): 实际上,Java中也有同步代码块,类似于Objective-C的`@synchronized`: ```java public void increment() { synchronized (this) { // 使用this作为锁对象 count++; } } ``` 五、总结: | 特性 | Java方法级`synchronized` | Objective-C `@synchronized` | |--------------|---------------------------------------------|-----------------------------------| | 语法形式 | 方法修饰符 | 代码块指令 | | 锁对象 | 隐式:实例方法为`this`,静态方法为`Class`对象 | 显式指定任意对象 | | 同步范围 | 整个方法体 | 代码块内 | | 异常释放锁 | 自动释放 | 自动释放 | | 性能考量 | 范围过大可能影响性能 | 相对较慢,但粒度细 | 因此,两者虽然都提供了同步机制,但属于不同语言,语法用法有显著区别。在Java中,更推荐使用显式的锁(如`ReentrantLock`)或并发工具类来替代粗粒度的同步方法;而在Objective-C中,`@synchronized`是常用的同步手段。 §§相关问题§§: 1. Java中除了`synchronized`关键字,还有哪些实现线程同步的方法? 2. Objective-C中`@synchronized`指令底层是如何实现的? 3. 在多线程编程中,如何避免死锁? 4. 比较Java的`synchronized``ReentrantLock`的异同点。 </think>### Java `synchronized` 方法与 Objective-C `@synchronized` 的区别详解 这两者虽然名称相似,但属于**不同语言的同步机制**: | 特性 | Java `synchronized` 方法 | Objective-C `@synchronized` | |------------------|---------------------------------------------|-------------------------------------| | **语言** | Java | Objective-C | | **作用范围** | 整个方法体 | 代码块内部 | | **锁对象** | 隐式(实例方法是`this`,静态方法是`Class`) | 显式指定任意对象 | | **粒度控制** | 粗粒度(整个方法锁定) | 细粒度(可锁定特定代码段) | | **性能影响** | 锁定期间整个方法不可访问 | 只锁定指定代码块 | | **异常处理** | 自动释放锁 | 自动释放锁 | --- ### 1. Java `synchronized` 方法 **特点**: - 作用于**整个方法** - 锁对象隐式确定: - 实例方法 → 锁定当前实例 (`this`) - 静态方法 → 锁定类对象 (`Class`对象) **代码示例**: ```java public class JavaExample { private int counter = 0; // 实例方法同步:锁对象 = this public synchronized void increment() { counter++; // 整个方法体被锁定 } // 静态方法同步:锁对象 = JavaExample.class public static synchronized void log() { System.out.println("Static method"); } } ``` **执行流程**: 1. 线程尝试获取实例对象(或类对象)的管程锁 2. 获取成功则执行方法体 3. 方法结束时自动释放锁 4. 其他线程需等待锁释放 --- ### 2. Objective-C `@synchronized` **特点**: - 作用于**代码块**而非整个方法 - **必需显式指定锁对象**(通常是`self`) - 使用更灵活,可精确控制同步范围 **代码示例**: ```objectivec @implementation ObjCExample { NSInteger _counter; } - (void)increment { // 非同步代码可并行执行 NSLog(@"Pre-sync"); @synchronized (self) { // 显式指定锁对象 _counter++; // 仅此代码块被锁定 } // 非同步代码可并行执行 NSLog(@"Post-sync"); } @end ``` **执行流程**: 1. 使用`objc_sync_enter`函数尝试获取指定对象的锁 2. 执行代码块内容 3. 使用`objc_sync_exit`函数自动释放锁 4. 代码块外的逻辑不受影响 --- ### 关键区别总结 | 维度 | Java `synchronized` 方法 | Objective-C `@synchronized` | |--------------|--------------------------------------|-----------------------------------| | **锁定范围** | 整个方法体 | 花括号`{}`包裹的代码块 | | **锁对象** | 隐式(无法更改) | 显式指定(可使用任意对象作为锁) | | **灵活性** | 低(无法控制方法内部分代码的同步) | 高(可精确控制关键代码段) | | **适用场景** | 简单同步需求 | 需要避免过度同步的性能敏感场景 | | **底层实现** | JVM管程锁 (monitor) | Objective-C运行时提供的互斥锁 | --- ### 何时选择哪种方式? **Java `synchronized` 方法适用场景**: - 整个方法都需要同步保护 - 简单计数器等基础同步需求 - 不需要精细控制锁范围的情况 **Objective-C `@synchronized` 适用场景**: - 方法中只有部分代码需要同步 - 需要锁定特定对象而非整个实例 - 性能敏感场景(避免不必要的锁定) - 需要同步多个相关对象时 > 📌 **最佳实践**:在Objective-C中优先使用`@synchronized`块而非整个`synchronized`方法,以减少锁竞争提高并发性能。
      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值