前言
我们需要锁,是因为多线程同时操作同一份数据时,可能会把数据改乱;锁的作用就是让关键代码在同一时刻只允许一个线程执行,从而保证数据一致性和线程安全。
这块是各类锁的性能比较
从高到底依次是:OSSpinLock(自旋锁) -> dispatch_semaphone(信号量) -> pthread_mutex(互斥锁) -> NSLock(互斥锁) -> NSCondition(条件锁) -> pthread_mutex(recursive 互斥递归锁) -> NSRecursiveLock(递归锁) -> NSConditionLock(条件锁) -> synchronized(互斥锁)
锁主要是干什么的
锁主要解决的是临界区问题,所谓临界区,就是多个线程都可能访问,并且一旦并发访问就可能出错的那段代码。就比如下面这段代码
objc
self.count++;
[self.array addObject:obj];
self.userInfo[@"name"] = @"Loky";
锁就是为了保证:同一时刻,只有一个线程可以进入这段临界区。
但是不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。
锁的分类
如果说我们从底层的视角出发的话所一般只有三种。
- 互斥锁(Mutex)
- 自旋锁(Spin Lock)
- 读写锁(Read-Write Lock)
其他我们如果按照另一个视角按照功能特性分类,那就是普通互斥锁,递归锁,条件锁,信号量,原子锁这些分类。但是今天我们按照底层的视角出发来看。
互斥锁
互斥锁是 iOS 中最基础、最常用的一类锁,核心机制是"拿不到锁就睡觉等":同一时刻只允许一个线程进入临界区,其他线程会被挂起,直到锁释放后被系统唤醒。这里面的关键特征就是阻塞 + 睡眠,就比如
线程 A 拿到锁 → 进入临界区
线程 B 申请锁失败 → 不再占用 CPU,进入 sleep
线程 A 解锁 → 操作系统唤醒线程 B
线程 B 拿到锁 → 进入临界区
这种机制带来两个特点:
- 不浪费 CPU:等锁的线程是睡眠的,CPU 可以去跑别的任务
- 有上下文切换开销 :每次睡眠和唤醒,都要切换线程上下文,成本不算低
所以互斥锁适合临界区耗时较长的场景。
互斥锁的两个子分类
| 类型 | 含义 | 同一线程是否能多次加锁 | 代表实现 |
|---|---|---|---|
| 非递归锁 | 一把锁同一时刻只允许一次加锁 | 否,会自己锁死自己 | NSLock、默认的 pthread_mutex |
| 递归锁(可重入锁) | 同一线程可以多次加锁,内部计数 | 是 | NSRecursiveLock、@synchronized、pthread_mutex(recursive) |
非递归互斥锁
核心机制
非递归锁的规则非常简单粗暴:一把锁同一时刻只允许被加锁一次 。哪怕是持有这把锁的线程自己,再次申请也会被当成"别人"------它会发现锁已经被占,于是进入睡眠等待。
问题就在这里:它等的那个"持有者"恰好就是自己。自己等自己解锁,但自己又被卡在等待里没法去执行解锁代码,于是死锁。
objc
线程 A 加锁 → 成功
线程 A 再次加锁 → 发现锁被占,进入睡眠
↓
永远等不到自己解锁
↓
死锁
pthread_mutex
pthread_mutex 是 POSIX 原生互斥锁,默认初始化方式得到的就是非递归锁,性能在所有互斥锁里最高:
objc
#import <pthread.h>
pthread_mutex_t _lock;
//造锁
pthread_mutex_init(&_lock, NULL); // NULL = 默认属性 = 非递归
pthread_mutex_lock(&_lock);
// 临界区
pthread_mutex_unlock(&_lock);
pthread_mutex_destroy(&_lock);
NSLock
NSLock 是对 pthread_mutex 默认模式的 OC 封装,是 iOS 里最典型的"非递归互斥锁":
objc
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// 临界区
[lock unlock];
在这里我们可以给出一个递归使用的例子
objc
NSLock *lock = [[NSLock alloc] init];
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d", value);
testMethod(value - 1); // 同一线程递归调用,再次申请同一把锁
}
[lock unlock];
};
testMethod(10);
运行结果是只打印出一次 current value = 10 就卡死了------因为第二次 [lock lock] 时,外层的锁还没释放,线程把自己锁死在了递归里

非递归锁的使用边界
适合:
- 临界区不嵌套调用自己
- 不会出现"同一线程同一把锁多次加锁"的情况
- 追求简洁、性能要好
不适合: - 递归函数中加锁
- 互相调用的多个加锁方法(A 拿了锁去调 B,B 又要拿同一把锁)
NSCondition(条件锁)
NSCondition 是"互斥锁 + 条件变量"的二合一封装,用于"等某个条件成立再继续"的场景。
NSCondition 在底层是 pthread_mutex_t + pthread_cond_t 的封装,本质上它既是一把互斥锁,又是一个'通知机制。
objc
@interface NSCondition : NSObject <NSLocking>
- (void)lock;
- (void)unlock;
- (void)wait; // 阻塞等待通知
- (BOOL)waitUntilDate:(NSDate *)limit; // 带超时的等待
- (void)signal; // 唤醒一个等待的线程
- (void)broadcast; // 唤醒所有等待的线程
@end
这个是里面能调用的方法。
这个是相关用法
objc
//初始化
NSCondition *condition = [[NSCondition alloc] init]
//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition lock];
//让当前线程处于等待状态
[condition wait];
//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];
//与lock 同时使用
[condition unlock];
但是这有几个要点
[condition lock]; // 当前线程持有锁
[condition wait]; // wait 内部:
// 1. 释放锁
// 2. 当前线程睡眠
// 3. 被 signal/broadcast 唤醒
// 4. 重新获取锁
// 5. wait 返回
// 能走到这里,说明当前线程又重新持有锁了
// 所以后面的代码仍然是受锁保护的
[condition unlock]; // 最终释放锁
要点1:wait 之前必须先 lock
wait 的语义是"原子地释放锁并睡觉",所以调用它之前必须已经持有锁 。否则行为未定义,可能直接崩溃。
要点2:signal 之前也必须先 lock
signal 本身在 pthread 层不强制要求持锁,但强烈建议在锁内调用------否则会出现"修改条件"和"发通知"之间被插入"消费者检查条件"的窗口,导致通知丢失。
objc
@interface Queue : NSObject
@property (nonatomic, strong) NSCondition *cond;
@property (nonatomic, strong) NSMutableArray *items;
@end
@implementation Queue
- (instancetype)init {
if (self = [super init]) {
_cond = [[NSCondition alloc] init];
_items = [NSMutableArray array];
}
return self;
}
// 消费者
- (id)take {
[self.cond lock];
while (self.items.count == 0) { // 注意是 while 不是 if
[self.cond wait]; // 没东西,释放锁睡觉
}
id obj = self.items.firstObject;
[self.items removeObjectAtIndex:0];
[self.cond unlock];
return obj;
}
// 生产者
- (void)put:(id)obj {
[self.cond lock];
[self.items addObject:obj];
[self.cond signal]; // 通知一个消费者
[self.cond unlock];
}
@end
这是一个完整的例子
他的本质也就是让一个线程在条件不满足时先去睡觉,等另一个线程把条件改好了,叫醒它继续干活。
NSConditionLock(条件锁)
NSConditionLock 是建立在 NSCondition 之上的一个带"状态值"的锁,可以这样理解我要在锁的状态等于 X 的时候才加锁;解锁时,把锁的状态设为 Y
objc
@interface NSConditionLock : NSObject <NSLocking>
- (instancetype)initWithCondition:(NSInteger)condition;
@property (readonly) NSInteger condition;
- (void)lock; // 不管状态,能拿就拿
- (void)unlock; // 解锁但不改状态
- (void)lockWhenCondition:(NSInteger)condition; // 等到状态等于 condition 才加锁
- (void)unlockWithCondition:(NSInteger)condition;// 解锁并把状态设为 condition
- (BOOL)tryLock; // 尝试加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end
这个是相关的 API
objc
//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
//表示 conditionLock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
[conditionLock lock];
//表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
[conditionLock lockWhenCondition:A条件];
//表示释放锁,同时把内部的condition设置为A条件
[conditionLock unlockWithCondition:A条件];
// 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理
return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间];
//其中所谓的condition就是整数,内部通过整数比较条件
NSConditionLock,其本质就是NSCondition + Lock
对于NSCondition的一个再次的封装
NSConditionLock可以设置锁条件,即condition值,而NSCondition只是信号的通知
递归互斥锁
核心机制
递归锁解决的就是非递归锁那个"自己锁死自己"的问题。它的关键设计是内部维护一个加锁计数器
就比如这样
objc
线程 A 加锁 → 计数 0 → 1,成功
线程 A 再加锁 → 发现持有者就是自己,计数 1 → 2,直接通过
线程 A 解锁 → 计数 2 → 1
线程 A 再解锁 → 计数 1 → 0,真正释放
这里面有两个关键点
- 必须是同一个线程才能重入。别的线程申请,照样要睡眠等待
- 加锁次数
pthread_mutex(recursive 模式)
这个区别于前面的pthread_mutex,他是在初始化的时候通过了pthread_mutexattr_t 把它的"行为类型"改成了 PTHREAD_MUTEX_RECURSIVE,让它支持同一线程的重入。
下面我给出一个例子
objc
pthread_mutex_t _lock;
// 1 声明锁本体
pthread_mutexattr_t attr;
// 2 声明锁的"属性配置器"
pthread_mutexattr_init(&attr);
// 3 初始化属性配置器
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 4 把类型设为递归
pthread_mutex_init(&_lock, &attr);
// 5 用这个属性来初始化锁
pthread_mutexattr_destroy(&attr);
// 6 属性配置器用完了销毁
在这里有两个东西
| 名字 | 类型 | 作用 |
|---|---|---|
_lock |
pthread_mutex_t |
锁本体,真正用来加锁解锁的对象 |
attr |
pthread_mutexattr_t |
锁的配置说明书,只在创建锁的那一瞬间起作用,告诉系统"我要造一把什么样的锁" |
pthread_mutex_t _lock; 声明一个锁变量。这一行只是分配了空间,里面是未初始化的脏数据,还不能用。 |
||
pthread_mutexattr_t attr; 声明属性结构体,同样是未初始化的状态。 |
||
pthread_mutexattr_init(&attr); 把 attr 初始化为系统默认值。此时 attr 里携带的类型是 PTHREAD_MUTEX_DEFAULT(在 macOS/iOS 上等价于 PTHREAD_MUTEX_NORMAL,也就是非递归)。 |
||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 在这段代码之前都没有初始化 attr默认携带的是PTHREAD_MUTEX_DEFAULT,但是这段代码把PTHREAD_MUTEX_RECURSIVE给了 attr,让他能够实现递归的功能。 |
||
pthread总共支持四种类型 |
| 常量 | 行为 |
|---|---|
PTHREAD_MUTEX_NORMAL |
非递归。同一线程二次加锁直接死锁 |
PTHREAD_MUTEX_RECURSIVE |
递归。同一线程可以多次加锁,内部计数 |
PTHREAD_MUTEX_ERRORCHECK |
非递归,但二次加锁会返回错误码而不是死锁,便于排查 |
PTHREAD_MUTEX_DEFAULT |
由系统决定,在 macOS/iOS 上等同于 NORMAL |
pthread_mutex_init(&_lock, &attr); 真正"造锁" ------把 _lock 按照 attr 描述的方式初始化。从这一行起,_lock 才是一把可以用的递归锁。在这之前都是配置锁的的相关内容。
pthread_mutexattr_destroy(&attr); 图纸已经发挥完作用了,回收资源。这里面有个点就是销毁 attr 不会影响 _lock ,因为 pthread_mutex_init 是把属性"复制"进锁的内部状态,不是引用。
前面就是配置锁和创建锁的过程。
他的使用和非递归的没有什么大的区别。
objc
pthread_mutex_lock(&_lock);
// 临界区
pthread_mutex_unlock(&_lock);
// 程序退出前
pthread_mutex_destroy(&_lock);
还是这一套。
下面我们把两套代码合一块可以更直观的看出来和非递归的区别
objc
pthread_mutex_t _lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&_lock); // 第 1 次:lockCount = 1
pthread_mutex_lock(&_lock); // 第 2 次:同一线程,lockCount = 2,直接放行
pthread_mutex_unlock(&_lock); // lockCount = 1
pthread_mutex_unlock(&_lock); // lockCount = 0,真正释放
这里面有一个很重要的规则,就是加锁次数一定要等于解锁次数,一次都不能少不然会被永久锁住。
NSRecursiveLock ------ OC 风格的递归锁
NSRecursiveLock 本质上就是你前面学的 pthread_mutex(recursive) 的 OC 封装,他的底层和 NSLock一样都是pthread_mutex,差别只在初始化时有没有设那个 PTHREAD_MUTEX_RECURSIVE 类型 ------这正是 pthread_mutexattr_settype 那一行干的事。NSRecursiveLock 等于帮你把那一套配置代码包好了。
这块的用法和 NSLock一模一样
objc
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d", value);
testMethod(value - 1); // 同一线程递归调用,再次申请同一把锁
}
[lock unlock];
};
testMethod(10);
下面是实现结果

但是如果我们把 block 放到异步的函数下执行的话会发现

他直接死锁了,这是什么原因导致的死锁呢,其实就是一个缠绕问题当多个线程同时往一把递归锁上挤,递归过程中又夹着 NSLog 这种重操作,调度器频繁切换,所有线程都在反复"加锁 → 等 → 切走 → 唤醒 → 再加锁"之间打转,整体看起来就像卡住了。这个最简单的解决方法就是把 lock 换成@synchronized就是随便写一个对象,
objc
@synchronized (lockObj) { // ← 关键改动
if (value > 0) {
NSLog(@"current value = %d", value);
testMethod(value - 1);
}
}
把代码写成这个样子@synchronized他会自动的解锁和加锁。
@synchronized ------ 自动管理的递归互斥锁
@synchronized 是 OC 里写法最简洁、用得最频繁的递归互斥锁:
objc
@synchronized (someObject) {
// 临界区
}
它的核心机制是:以传入的对象作为 key,在底层全局哈希表里查找或创建一把对应的 recursive_mutex_t,再配合 lockCount 计数实现重入。它最大的特点是"自动管理"------锁的查找、复用、加解锁配对全部由运行时帮你完成。
它在底层走了objc_sync_enter / objc_sync_exit,
objc
@synchronized (obj) {
// 临界区
}
// 编译后大致等价于:
objc_sync_enter(obj);
@try {
// 临界区
} @finally {
objc_sync_exit(obj);
}
@finally 保证了无论临界区怎么退出,objc_sync_exit 一定会执行------这就是"自动解锁"的实现机制。
下面是objc_sync_enter objc_sync_exit的源码
objc
int objc_sync_enter(id obj) {
if (obj) { // ① obj 不为 nil
SyncData* data = id2data(obj, ACQUIRE); // ② 关键:查/建锁
data->mutex.lock(); // ③ 真正加锁
} else { // ④ obj 为 nil
// @synchronized(nil) does nothing
objc_sync_nil();
}
return OBJC_SYNC_SUCCESS;
}
int objc_sync_exit(id obj) {
if (obj) {
SyncData* data = id2data(obj, RELEASE);
bool okay = data->mutex.tryUnlock();
// ...
} else {
// 同样什么都不做
}
}
可以看到,加锁和解锁复用同一个 id2data 方法 ,靠传入的 ACQUIRE / RELEASE 枚举值区分。
下面是 SyncData 还有 SyncCacheItem
objc
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; // 链表下一个节点
DisguisedPtr<objc_object> object; // 这把锁是给哪个对象用的
int32_t threadCount; // 当前有几个线程在用这把锁
recursive_mutex_t mutex; // ← 真正的递归互斥锁
} SyncData;
typedef struct {
SyncData *data;
unsigned int lockCount; // 当前线程对这把锁加了几层
} SyncCacheItem;
每个被 @synchronized 锁过的对象,都对应一个 SyncData。里面包了一把 recursive_mutex_t------这就是 @synchronized 是"递归互斥锁"的源头证据 。
SyncCacheItem他是记录次数的,这里面的 lockCount 是每个线程独立 的------同一把锁,T1 加了 3 次,T2 加了 1 次,每个线程的小账本各管各的。
这个时候我们可以看一下这两个结构体里面的 count他们干啥了
threadCount(在 SyncData 里,全局共享):这把锁有多少个线程在用 → 管理多线程并发-
lockCount(在 SyncCacheItem 里,每线程独立) :当前线程加了几层 → 管理同一线程的递归重入
我们看完了这两个结构体,然后再看一下id2data 的查找流程
objc
static SyncData* id2data(id object, enum usage why) //枚举值和锁住的对象传入
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS //(tls) 本地的局部线程缓存
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
//通过KVC方式对线程进行获取,线程绑定的data
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
//如果线程缓存中有data,执行if流程
if (data) {
fastCacheOccupied = YES;
//如果在线程空间找到了data
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
//通过KVC获取lockCount,lockCount用来记录被锁了几次,即该锁可嵌套
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {
//objc_sync_enter走这里,传入的是ACQUIRE -- 获取
lockCount++;//通过lockCount判断被锁了几次,即表示 可重入
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//设置
break;
}
case RELEASE:
//objc_sync_exit走这里,传入的why是RELEASE -- 释放
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
//移除快速缓存的部分
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);//判断缓存中是否有该线程
//如果cache中有,方法与线程缓存一致.
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) { //遍历总表
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE: //加锁
item->lockCount++;
break;
case RELEASE: // 解锁
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
//清楚cache中清楚标识
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
//第一次进入,所有缓存都找不到
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) { //cache中找到了
if ( p->object == object ) { //如果不等于空,且与object相同
result = p; //赋值
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
//没有与当前对象关联的SyncData
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
//第一次进入,没有找到
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); //创建赋值
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) { // 判断是否支持栈存缓存,支持KVC形式赋值存入tls
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
在这里我们可以看下他的查找逻辑
objc
传入对象 obj
↓
① 查 tls(线程本地存储,最快)
tls_get_direct(SYNC_DATA_DIRECT_KEY) → 当前线程上次锁的 obj
如果就是当前传入的 obj → lockCount++ 返回(极快路径)
↓ 不是
② 查 cache(线程级缓存)
遍历当前线程缓存的所有 SyncCacheItem
找到 item->data->object == obj → lockCount++ 返回
↓ 没找到
③ 查全局链表(哈希表 + 拉链法)
以 obj 为 key 在全局 SyncList 里找
找到 → 复用现有 SyncData,threadCount++
没找到 → posix_memalign 创建新的 SyncData 挂到链表上
↓
把结果存进 tls 或 cache,下次走快速路径
下面我们给一张图来帮助理解

我们可以举个很简单的例子,什么时候会走到第一步什么时候会走到第二步还有第三步
objc
NSObject *objA = [NSObject new]; // 比方说是一个"账户"
NSObject *objB = [NSObject new]; // 另一个"账户"
// ===== 第 ① 段 =====
@synchronized (objA) {
NSLog(@"操作 objA");
}
// ===== 第 ② 段 =====
@synchronized (objA) {
NSLog(@"再次操作 objA");
}
// ===== 第 ③ 段 =====(嵌套)
@synchronized (objA) {
@synchronized (objB) { // ← 看这一行
NSLog(@"嵌套:外层 objA,内层 objB");
}
}
// ===== 第 ④ 段 =====
@synchronized (objA) {
@synchronized (objB) { // ← 关键看这一行
NSLog(@"再次嵌套");
}
}
// ===== 第 ⑤ 段 =====
NSObject *objC = [NSObject new]; // 又一个新对象
@synchronized (objC) {
NSLog(@"全新对象 objC");
}
| 代码段 | 操作 | 命中级别 | 原因 |
|---|---|---|---|
| ① | 第一次锁 objA | 第三步 | 全新对象,全局表里也没有,必须新建 |
| ② | 紧接着再锁 objA | 第一步(TLS) | 上次刚锁过,TLS 里就是它 |
| ③ 内层 | 嵌套锁全新的 objB | 第三步 | 又一个全新对象,必须新建 |
| ④ 内层 | 嵌套又锁 objB | 第二步(Cache) | TLS 被外层 objA 占着,但 Cache 里有 objB |
| ⑤ | 锁全新的 objC | 第三步 | 还是全新对象 |
小结
@synchronized 真正提供互斥能力的是 SyncData 里那把递归锁;TLS、Cache、全局表分桶这一整套机制,纯粹是为了让'根据对象找到这把锁'这件事尽可能快------三级查找是查找速度的优化,不是锁功能的一部分。
自旋锁
自旋锁和互斥锁最大的区别是 CPU 的等待
普通互斥锁拿不到锁时:
拿不到锁 → 线程进入睡眠/阻塞 → CPU 去执行别的线程 → 锁释放后再唤醒
自旋锁拿不到锁时:
拿不到锁 → 不睡觉 → 一直 while 循环检查 → 锁一释放立刻抢
os_unfair_lock
objc
#import <os/lock.h>
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&lock);
// 临界区
os_unfair_lock_unlock(&lock);
os_unfair_lock 名字里没有 spin,它不是传统意义上的纯自旋锁。它的设计目标是替代 OSSpinLock,解决优先级反转问题,拿不到锁时,线程会进入等待/休眠,而不是一直忙等烧 CPU
atomic
Objective-C 的 atomic 属性在 setter/getter 里会对属性槽位加锁,保证一次 getter 或 setter 是原子的。
比如
objc
@property (atomic, strong) NSString *name;
它大概能保证:
objc
self.name = @"A"; // setter 内部加锁
NSString *x = self.name; // getter 内部加锁
但是它不能保证复合操作安全:
objc
self.count = self.count + 1;
因为这行代码拆开是
- getter 读取 count
- +1
- setter 写回 count
getter 和 setter 各自可能是安全的,但整个"读-改-写"不是一个整体,中间可能被别的线程插入。
即使atomic内部用了锁,它也不能替代你手动保护整个临界区。
总结
自旋锁:
拿不到锁时不睡觉,一直占 CPU 等。
优点是没有线程睡眠/唤醒开销,短临界区很快。
缺点是等待时间一长就浪费 CPU,还可能有优先级反转问题。
互斥锁:
拿不到锁时线程睡眠,CPU 去干别的。
优点是不会忙等,适合大多数业务场景。
缺点是睡眠/唤醒有上下文切换开销。
-
OSSpinLock因为存在安全问题,在 iOS 10 之后已经不推荐使用,主要问题是它在等待锁时会一直忙等,占用 CPU,容易造成优先级反转。后来底层更多使用os_unfair_lock来替代它。os_unfair_lock在拿不到锁时不会一直空转,而是让线程进入等待/休眠状态,因此比OSSpinLock更安全。 -
atomic属性内部也会通过加锁来保证 setter 和 getter 的单次读写安全,但它只能保证"单次取值"或"单次赋值"是线程安全的,不能保证复合操作安全,例如self.count = self.count + 1这种"读 → 改 → 写"的整体过程仍然可能出问题。因此日常开发中,大多数属性仍然使用nonatomic,如果需要真正的线程安全,要额外加锁或使用其他同步手段。 -
@synchronized底层会通过哈希表和链表维护对象对应的锁信息,并且支持可重入,也就是同一个线程可以对同一个对象重复加锁而不会死锁。它使用简单,但因为底层查找、缓存、递归锁等机制较复杂,所以性能相对较低。 -
NSLock和NSRecursiveLock本质上都是对pthread_mutex的封装。NSLock是普通互斥锁,不支持同一线程重复加锁;NSRecursiveLock是递归锁,允许同一线程多次加锁,适合递归或嵌套调用场景。 -
NSCondition和NSConditionLock也是基于pthread_mutex封装出来的条件锁,用来处理"满足某个条件后线程才能继续执行"的场景。它们不仅能加锁,还能让线程等待和被唤醒,作用和dispatch_semaphore有些相似,都可以用于线程同步和控制执行顺序。
使用场景
- 普通线程安全用
NSLock; - 递归/嵌套加锁用
NSRecursiveLock或@synchronized; - 线程要等某个条件再继续用
NSCondition; - 要按状态顺序执行用
NSConditionLock; - 底层高性能短临界区用
os_unfair_lock; - 多读单写优先用并发队列 + barrier。