@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. 递归锁的执行流程
假设你在执行一段递归代码:
-
第一次 Enter:
- 查找哈希表获取
SyncData。 mutex.lock()成功。- TLS 记录该锁,
lockCount = 1。
- 查找哈希表获取
-
第二次 Enter(递归):
- 检查 TLS,命中!
lockCount变为2。- 直接进入临界区,开销极小。
-
退出 (Exit):
- 检查 TLS,
lockCount递减。 - 直到
lockCount归零,才调用底层的mutex.unlock(),让其他线程有机会竞争。
- 检查 TLS,
4. 为什么递归锁如此重要?
如果没有递归支持,下面的代码会直接导致程序卡死:
Objective-C
- (void)methodA {
@synchronized(self) {
[self methodB]; // 如果不支持递归,这里会死锁
}
}
- (void)methodB {
@synchronized(self) {
// 执行逻辑
}
}
在复杂的面向对象设计中,方法之间经常会互相调用。@synchronized 通过递归锁机制,让开发者无需担心这种内部逻辑嵌套导致的死锁问题。
总结
@synchronized 的递归性是由 递归互斥锁(Recursive Mutex) 保证的,并利用 TLS(线程局部存储) 进行了大幅度性能优化。它像是一个打卡机:同一个员工(线程)进来时,机器只是在卡片上多盖一个章(计数加 1),而不会把你拦在大门外。