4-7.【OC】【锁】objc_sync_enter 内部如何根据对象找到对应的锁?

4 阅读3分钟

objc_sync_enter 的底层实现中,Objective-C 并没有为每个对象直接分配一个锁(那太浪费内存了),而是采用了一种全局哈希映射的机制。

它的核心逻辑可以概括为:通过对象的内存地址作为 Key,去一个全局的“锁池”里找(或创建一个)递归锁。


1. 核心数据结构:SyncData

objc 源码中,每个被 @synchronized 使用的对象都会对应一个 SyncData 结构体:

C++

typedef struct SyncData {
    struct SyncData* nextData; // 链表结构,处理哈希冲突
    id               object;   // 锁对应的目标对象
    int              threadCount; // 有多少个线程正在使用这个锁
    recursive_mutex_t mutex;   // 真正的递归锁
} SyncData;

2. 查找过程:三层缓存机制

为了平衡“空间占用”和“查找速度”,objc_sync 内部设计了极其复杂的缓存策略:

第一层:线程局部缓存 (Thread Local Storage, TLS)

当一个线程尝试获取锁时,它首先检查自己的 TLS。如果该线程最近刚刚访问过这个对象的锁,它会直接从私有缓存中拿到 SyncData。这大大加快了同一线程频繁重入(Recursive Call)的速度。

第二层:线程缓存快照 (Cache Snapshot)

如果 TLS 没中,它会检查一个专门存储最近使用的 SyncData 的缓存列表。

第三层:全局哈希表 (StripedMap)

如果前两层都落空了,就会进入最后的“大本营”:sDataLists

  • 这是一个全局的哈希表(具体实现为 StripedMap<SyncList>)。
  • 哈希计算: 系统对对象的内存地址执行哈希算法,计算出它应该落在哪个“桶(Bucket)”里。
  • 处理冲突: 不同的对象可能会哈希到同一个桶。这时,桶内部会通过一个单向链表(即 SyncData 中的 nextData)来存储多个不同的锁对象。

3. 锁的生命周期管理

既然是全局映射,就涉及到锁的释放问题。如果对象销毁了,锁还在哈希表里占位怎么办?

  1. 引用计数: SyncData 内部记录了 threadCount
  2. 复用机制: 当没有线程再使用某个 SyncData 时(threadCount == 0),该结构体并不会立即被销毁,而是会被标记为“可复用”。
  3. 重新分配: 下次有新对象需要锁时,会优先从这个复用池里捞出一个 SyncData,把里面的 object 替换掉。这样避免了频繁的内存分配和锁的销毁开销。

4. 为什么要这样设计?

这种“侧边栏”式的设计有两大好处:

  1. 零内存成本: 对象如果不使用 @synchronized,它就完全不需要承担锁的内存开销。对象头不需要增加任何字段。
  2. 兼容性: 任何 Objective-C 对象(甚至是从 C 传过来的对象指针)都可以作为锁的 Key。

但是缺点也显而易见:

  • 哈希瓶颈: 如果大量线程同时操作不同的对象,但这些对象不幸落到了同一个哈希桶里,它们会产生虚假的锁竞争。
  • 性能损耗: 相比于在对象内部直接放一个锁(如 Swift 的某些内部机制),哈希查找和 TLS 维护的开销要大得多。

总结

objc_sync_enter 就像是一个公共储物柜管理器: 你(对象)没有柜子,你带着你的身份证(地址)去前台。前台查了一下记录(哈希表),给你分配了一个储物柜(递归锁)。当你不用了,柜子会被收回,留给下一个带身份证来的人。