4-10.【OC】【锁】@synchronized 的性能瓶颈主要来自哪几层?

2 阅读3分钟

虽然 @synchronized 在易用性上拿到了满分,但它的性能在 iOS 各类锁中通常排在末尾。其性能瓶颈并非来自单一原因,而是源于从语言层到底层硬件的“多重收税”


1. 全局竞争层:哈希桶冲突 (The Hash Table Tax)

正如前面提到的,@synchronized 使用一个全局的 StripedMap 来存储锁。

  • 虚假竞争 (False Contention): 这个哈希表的大小是固定的(通常是 64 或 128 个桶)。如果两个完全无关的对象(对象 A 和对象 B)恰好哈希到了同一个桶里,即便它们处于不同的线程,在获取锁时也必须竞争桶级别的内部互斥锁。
  • 全局锁瓶颈: 操作这个全局哈希表本身需要加锁(SpinLock),这意味着在高并发场景下,仅仅是“寻找那把锁”的过程就可能产生阻塞。

2. 逻辑复杂度层:TLS 与缓存维护 (The Bookkeeping Tax)

为了支持递归和自动释放,系统做了大量的记录工作:

  • TLS 查找: 每次 enterexit 都要读写线程局部存储(Thread Local Storage),以维护递归计数值。
  • 生命周期管理: 系统需要检查 SyncDatathreadCount,判断锁是否可以被回收或复用。这种精细的内存管理在频繁调用时累积开销巨大。

3. 安全性层:异常处理开销 (The Exception Handling Tax)

这是 @synchronized 最大的隐形成本。

  • 为了保证代码块内崩溃时锁能正常释放,编译器会生成一套 Try-Catch-Finally 指令。
  • ARC 干扰: 在 Objective-C 中,异常处理会干扰编译器对 ARC 的优化,可能导致额外的 retain/release 操作。
  • 指令膨胀: 相比于 os_unfair_lock 仅仅几行汇编指令,@synchronized 展开后的机器码量要大得多,这不仅增加了执行时间,也对 CPU 的指令缓存(I-Cache)不友好。

4. 锁类型层:递归锁的固有开销 (The Recursive Lock Tax)

@synchronized 底层使用的是递归锁(Recursive Mutex),而非简单的互斥锁。

  • 状态检查: 递归锁每次加锁都要检查: “这把锁现在是谁持有的?是不是当前线程?”
  • 重量级系统调用:os_unfair_lock 这种在用户态自旋、必要时才挂起的轻量锁相比,recursive_mutex 更多地依赖于 pthread_mutex,这涉及到更多的内核态切换开销。

5. 性能对比:谁更快?

我们可以通过下表直观地看到 @synchronized 的位置:

锁类型性能位次核心开销点适用场景
OSAtomic / Atomic1 (最快)硬件级指令 (CAS)简单变量修改
os_unfair_lock2极低,不涉及内核切换iOS 高频竞争首选
dispatch_semaphore3信号量内核同步控制并发数
NSLock4封装 pthread_mutex普通封装需求
@synchronized5 (最慢)哈希查找 + 递归维护 + 异常捕获极其偶尔的简单同步

总结与实战建议

@synchronized 的瓶颈在于它为了**“绝对的易用性” (支持递归、支持 nil、支持异常安全、无需手动创建锁对象)牺牲了“极致的性能”**。

我的建议:

  • 如果你在写 UI 组件或高频操作(如每秒执行上千次的缓存读取),请远离 @synchronized,改用 os_unfair_lock
  • 如果你在写 初始化逻辑低频配置更新,且不确定是否存在递归调用,@synchronized 是安全的捷径。