虽然 @synchronized 在易用性上拿到了满分,但它的性能在 iOS 各类锁中通常排在末尾。其性能瓶颈并非来自单一原因,而是源于从语言层到底层硬件的“多重收税” 。
1. 全局竞争层:哈希桶冲突 (The Hash Table Tax)
正如前面提到的,@synchronized 使用一个全局的 StripedMap 来存储锁。
- 虚假竞争 (False Contention): 这个哈希表的大小是固定的(通常是 64 或 128 个桶)。如果两个完全无关的对象(对象 A 和对象 B)恰好哈希到了同一个桶里,即便它们处于不同的线程,在获取锁时也必须竞争桶级别的内部互斥锁。
- 全局锁瓶颈: 操作这个全局哈希表本身需要加锁(SpinLock),这意味着在高并发场景下,仅仅是“寻找那把锁”的过程就可能产生阻塞。
2. 逻辑复杂度层:TLS 与缓存维护 (The Bookkeeping Tax)
为了支持递归和自动释放,系统做了大量的记录工作:
- TLS 查找: 每次
enter和exit都要读写线程局部存储(Thread Local Storage),以维护递归计数值。 - 生命周期管理: 系统需要检查
SyncData的threadCount,判断锁是否可以被回收或复用。这种精细的内存管理在频繁调用时累积开销巨大。
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 / Atomic | 1 (最快) | 硬件级指令 (CAS) | 简单变量修改 |
| os_unfair_lock | 2 | 极低,不涉及内核切换 | iOS 高频竞争首选 |
| dispatch_semaphore | 3 | 信号量内核同步 | 控制并发数 |
| NSLock | 4 | 封装 pthread_mutex | 普通封装需求 |
| @synchronized | 5 (最慢) | 哈希查找 + 递归维护 + 异常捕获 | 极其偶尔的简单同步 |
总结与实战建议
@synchronized 的瓶颈在于它为了**“绝对的易用性” (支持递归、支持 nil、支持异常安全、无需手动创建锁对象)牺牲了“极致的性能”**。
我的建议:
- 如果你在写 UI 组件或高频操作(如每秒执行上千次的缓存读取),请远离
@synchronized,改用os_unfair_lock。- 如果你在写 初始化逻辑 或 低频配置更新,且不确定是否存在递归调用,
@synchronized是安全的捷径。