[OC 底层] (五) iOS 中常见的几种锁

前言

我们需要锁,是因为多线程同时操作同一份数据时,可能会把数据改乱;锁的作用就是让关键代码在同一时刻只允许一个线程执行,从而保证数据一致性和线程安全。​

这块是各类锁的性能比较

从高到底依次是:OSSpinLock(自旋锁) -> dispatch_semaphone(信号量) -> pthread_mutex(互斥锁) -> NSLock(互斥锁) -> NSCondition(条件锁) -> pthread_mutex(recursive 互斥递归锁) -> NSRecursiveLock(递归锁) -> NSConditionLock(条件锁) -> synchronized(互斥锁)

锁主要是干什么的

锁主要解决的是临界区问题,所谓临界区,就是多个线程都可能访问,并且一旦并发访问就可能出错的那段代码。就比如下面这段代码

objc 复制代码
self.count++;
[self.array addObject:obj];
self.userInfo[@"name"] = @"Loky";

锁就是为了保证:同一时刻,只有一个线程可以进入这段临界区。

但是不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了

锁的分类

如果说我们从底层的视角出发的话所一般只有三种。

  1. 互斥锁(Mutex)
  2. 自旋锁(Spin Lock)
  3. 读写锁(Read-Write Lock)
    其他我们如果按照另一个视角按照功能特性分类,那就是普通互斥锁,递归锁,条件锁,信号量,原子锁这些分类。但是今天我们按照底层的视角出发来看。

互斥锁

互斥锁是 iOS 中最基础、最常用的一类锁,核心机制是"拿不到锁就睡觉等":同一时刻只允许一个线程进入临界区,其他线程会被挂起,直到锁释放后被系统唤醒。​这里面的关键特征就是阻塞 + 睡眠,就比如

线程 A 拿到锁 → 进入临界区

线程 B 申请锁失败 → 不再占用 CPU,进入 sleep

线程 A 解锁 → 操作系统唤醒线程 B

线程 B 拿到锁 → 进入临界区

这种机制带来两个特点:

  • 不浪费 CPU:等锁的线程是睡眠的,CPU 可以去跑别的任务
  • 有上下文切换开销 :每次睡眠和唤醒,都要切换线程上下文,成本不算低
    所以互斥锁适合临界区耗时较长的场景。

互斥锁的两个子分类

类型 含义 同一线程是否能多次加锁 代表实现
非递归锁 一把锁同一时刻只允许一次加锁 否,会自己锁死自己 NSLock、默认的 pthread_mutex
递归锁(可重入锁)​ 同一线程可以多次加锁,内部计数 NSRecursiveLock@synchronizedpthread_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;

因为这行代码拆开是

  1. getter 读取 count
  2. +1
  3. 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 底层会通过哈希表和链表维护对象对应的锁信息,并且支持可重入,也就是同一个线程可以对同一个对象重复加锁而不会死锁。它使用简单,但因为底层查找、缓存、递归锁等机制较复杂,所以性能相对较低。

  • NSLockNSRecursiveLock 本质上都是对 pthread_mutex 的封装。NSLock 是普通互斥锁,不支持同一线程重复加锁;NSRecursiveLock 是递归锁,允许同一线程多次加锁,适合递归或嵌套调用场景。

  • NSConditionNSConditionLock 也是基于 pthread_mutex 封装出来的条件锁,用来处理"满足某个条件后线程才能继续执行"的场景。它们不仅能加锁,还能让线程等待和被唤醒,作用和 dispatch_semaphore 有些相似,都可以用于线程同步和控制执行顺序。

使用场景

  1. 普通线程安全用 NSLock
  2. 递归/嵌套加锁用 NSRecursiveLock@synchronized
  3. 线程要等某个条件再继续用 NSCondition
  4. 要按状态顺序执行用 NSConditionLock
  5. 底层高性能短临界区用 os_unfair_lock
  6. 多读单写优先用并发队列 + barrier。
相关推荐
技术小结-李爽5 小时前
Mac快捷键设计规律总结
macos·键盘
铁锚6 小时前
macOS 禁用 mediaanalysisd
macos·策略模式
库奇噜啦呼6 小时前
【iOS】源码学习-SDWebImage源码学习
学习·ios
zuYM4g7Dp7 小时前
文顶顶iOS开发数据库篇—SQL
数据库·sql·ios
超梦dasgg8 小时前
APP 壳、加固、脱壳 完整通俗讲解(安卓为主,兼顾 iOS)
android·ios
酉鬼女又兒8 小时前
零基础入门计算机网络:MAC地址、IP地址与ARP协议全面解析(含考研真题详解)
网络·网络协议·tcp/ip·计算机网络·考研·macos·职场和发展
资源分享助手8 小时前
PeekDesktop:实现类似 macOS Sonoma 点击桌面预览(Windows工具教程)
windows·macos·点击回到桌面
代码的小搬运工9 小时前
【iOS】MVC架构
ios·架构·mvc
鹤卿1239 小时前
iOS OC NSUserDefaults
macos·ios·objective-c