4-9.【OC】【锁】@synchronized 为什么天然支持递归?

3 阅读3分钟

@synchronized 之所以天然支持递归(即同一个线程可以多次获取同一把锁而不会导致死锁),是因为它在底层使用了 递归锁(Recursive Mutex) ,并且配合了**嵌套计数(Nesting Count)**机制。

在底层源码中,这主要通过 SyncData 结构体中的 recursive_mutex_t 以及线程局部存储(TLS)来实现。


1. 核心工具:recursive_mutex_t

objc-sync.mm 的实现里,每个 SyncData 对象内部持有的锁不是普通的互斥锁(Mutex),而是 recursive_mutex_t

  • 普通锁: 如果线程 A 已经持有了锁,再次尝试获取时,它会发现锁被占用,于是进入等待状态。但因为它自己占着锁不放,它会永远等待下去,形成自死锁
  • 递归锁: 它会检查当前尝试加锁的线程是否已经是该锁的拥有者。如果是,它直接允许通过,并将内部计数器加 1

2. 三级缓存与嵌套计数

为了高效支持递归,苹果在底层设计了精妙的缓存方案,确保系统知道“谁锁了多少次”:

第一级:线程局部存储 (TLS)

每个线程都有一个私有的 SyncStackTrace。当你第一次对 obj 加锁时,系统会将该锁的信息存入当前线程的 TLS 中。

  • 当你递归调用时,系统先查 TLS。
  • 如果发现 obj 已经在当前线程的锁名单里,它只需要简单地增加该条目的 lockCount,而不需要再去操作全局哈希表。

第二级:SyncData 内部的计数

除了 TLS 层的优化,底层的 SyncData 结构体也会参与维护:

  • threadCount:记录有多少个不同的线程在使用这个锁。
  • mutex.lock():底层的递归锁自身也维护着一个引用计数。只有当计数归零时(即 exit 的次数等于 enter 的次数),锁才会真正释放给其他线程。

3. 递归锁的执行流程

假设你在执行一段递归代码:

  1. 第一次 Enter:

    • 查找哈希表获取 SyncData
    • mutex.lock() 成功。
    • TLS 记录该锁,lockCount = 1
  2. 第二次 Enter(递归):

    • 检查 TLS,命中!
    • lockCount 变为 2
    • 直接进入临界区,开销极小。
  3. 退出 (Exit):

    • 检查 TLS,lockCount 递减。
    • 直到 lockCount 归零,才调用底层的 mutex.unlock(),让其他线程有机会竞争。

4. 为什么递归锁如此重要?

如果没有递归支持,下面的代码会直接导致程序卡死:

Objective-C

- (void)methodA {
    @synchronized(self) {
        [self methodB]; // 如果不支持递归,这里会死锁
    }
}

- (void)methodB {
    @synchronized(self) {
        // 执行逻辑
    }
}

在复杂的面向对象设计中,方法之间经常会互相调用。@synchronized 通过递归锁机制,让开发者无需担心这种内部逻辑嵌套导致的死锁问题。


总结

@synchronized 的递归性是由 递归互斥锁(Recursive Mutex) 保证的,并利用 TLS(线程局部存储) 进行了大幅度性能优化。它像是一个打卡机:同一个员工(线程)进来时,机器只是在卡片上多盖一个章(计数加 1),而不会把你拦在大门外。