iOS 开发必备:11 种锁全解析(含自旋 / 阻塞原理 + 实战场景)
在 iOS 多线程开发中,锁是解决资源竞争、数据错乱的核心手段,不同锁的底层实现、自旋 / 阻塞特性、性能和适用场景差异显著。本文将系统梳理 iOS 开发中最常用的 11 种锁,从核心原理、自旋 / 阻塞属性、代码示例到实战选型,一站式讲透锁的使用技巧,适配 OC/Swift 双开发语言,新手也能快速上手。
一、核心前置概念:自旋 vs 阻塞
所有锁的等待逻辑仅分两类,这是选锁的核心依据,先明确概念再学锁,理解更透彻:
🔹 自旋(忙等)
- 核心逻辑:线程抢锁失败时,不放弃 CPU 执行权,通过无限循环持续检查锁的状态,直到抢到锁为止(相当于 “站在门口死等”)。
- 关键特性:无线程休眠 / 唤醒成本,但若等待时间过长会导致 CPU 空转,造成设备卡顿、发热。
- 适用场景:临界区执行时间极短(如简单变量赋值、计数操作,几微秒完成)。
🔹 阻塞(闲等)
- 核心逻辑:线程抢锁失败时,主动放弃 CPU 执行权,进入休眠状态,系统会在锁释放后唤醒该线程,重新参与抢锁(相当于 “到旁边歇着,等通知再回来”)。
- 关键特性:会产生轻微的线程休眠 / 唤醒成本,但不会占用 CPU 空转,适配绝大多数开发场景。
- 适用场景:99% 的日常开发场景,尤其是临界区执行时间较长的操作(如数组 / 字典修改、数据解析)。
🔹 核心区别
| 对比维度 | 自旋(忙等) | 阻塞(闲等) |
|---|---|---|
| 等待时 CPU 占用 | 占用(循环检查,无实际工作) | 不占用(休眠,释放 CPU 给其他线程) |
| 线程状态 | 运行态 | 休眠态 |
| 系统开销 | 无休眠 / 唤醒成本 | 有轻微的休眠 / 唤醒成本 |
| 风险点 | 临界区过长会导致 CPU 空转、卡顿 | 几乎无风险,适配绝大多数场景 |
二、iOS 11 种锁全解析(按类型分类)
本次梳理包含自旋相关锁、纯阻塞锁、特殊队列锁三大类,覆盖底层 C 库、Foundation 框架、GCD 等实现方式,标注所有锁的自旋 / 阻塞属性、核心特性、OC/Swift 代码示例、实战场景,直接对标开发需求。
第一类:自旋相关锁(2 种,高性能短临界区专用)
仅 2 种与自旋相关,其中 1 种已废弃,核心使用苹果优化后的混合自旋锁,兼顾性能与安全性。
1. OSSpinLock(废弃,了解即可)
- 自旋 / 阻塞:纯自旋
- 底层实现:汇编实现,iOS 最早期的自旋锁
- 核心问题:存在优先级反转(低优先级线程持锁,高优先级线程自旋等待,导致低优先级线程无法获取 CPU,锁永远无法释放),iOS10 后被苹果废弃,开发中禁止使用。
2. os_unfair_lock(iOS10 + 推荐,自旋锁升级版)
-
自旋 / 阻塞:混合(短时间自旋 + 长时间阻塞)
-
底层实现:系统底层封装,苹果为解决 OSSpinLock 问题推出的替代方案
-
核心特性:先自旋几微秒尝试抢锁,抢不到则自动切换为阻塞模式,兼顾自旋的高性能和阻塞的低消耗,完美解决优先级反转问题;不可重入。
-
适用场景:临界区执行时间极短的高性能加锁场景(如简单变量赋值、计数操作)。
-
OC 代码示例:
objc
// 初始化 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // 加锁 os_unfair_lock_lock(&lock); // 临界区(共享资源操作) self.count++; // 解锁 os_unfair_lock_unlock(&lock); -
Swift 代码示例:
swift
var lock = os_unfair_lock() // 加锁 os_unfair_lock_lock(&lock) // 临界区 count += 1 // 解锁 os_unfair_lock_unlock(&lock)
第二类:纯阻塞锁(8 种,开发主力军,适配 99% 场景)
这 8 种锁均为纯阻塞模式,抢锁失败即休眠,是 iOS 开发的核心选择,底层多基于 pthread 库封装,按 “底层→上层、基础→高级” 排序,适配不同开发需求。
1. pthread_mutex_t(普通互斥锁)
-
自旋 / 阻塞:纯阻塞
-
底层实现:C 语言 pthread 库基础锁,iOS 最底层的互斥锁
-
核心特性:基础互斥锁,不可重入(同一线程多次加锁会死锁);性能拉满,无多余封装开销。
-
适用场景:底层高性能加锁场景,OC/Swift 底层开发均可使用。
-
OC 代码示例:
objc
pthread_mutex_t mutex; // 初始化 pthread_mutex_init(&mutex, NULL); // 加锁 pthread_mutex_lock(&mutex); // 临界区 [self.array addObject:@"data"]; // 解锁 pthread_mutex_unlock(&mutex); // 销毁 pthread_mutex_destroy(&mutex);
2. pthread_mutex_t(递归互斥锁)
-
自旋 / 阻塞:纯阻塞
-
底层实现:pthread 库,普通互斥锁的扩展
-
核心特性:通过设置递归属性实现可重入,加锁次数与解锁次数一致时,锁才会真正释放;解决普通互斥锁 “递归 / 嵌套加锁死锁” 问题。
-
适用场景:存在递归调用、同一方法内多次加锁的场景。
-
OC 初始化差异:
objc
pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); // 设置递归属性 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&mutex, &attr); pthread_mutexattr_destroy(&attr);
3. dispatch_semaphore_t(信号量锁,最通用)
-
自旋 / 阻塞:纯阻塞
-
底层实现:GCD 封装,通过信号量计数实现加锁
-
核心特性:可重入;计数 > 0 时允许线程执行,计数 = 0 时阻塞;计数 = 1 时为普通互斥锁,计数 = N 时可限制 N 个线程并行执行(并发锁),灵活性拉满。
-
适用场景:最通用的加锁场景,可替代大部分基础锁;还可用于限制最大并发数(如网络请求限制同时 5 个)。
-
OC 代码示例(互斥锁,计数 = 1) :
objc
// 初始化信号量,计数=1 dispatch_semaphore_t sema = dispatch_semaphore_create(1); // 加锁(计数-1,变为0则阻塞) dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // 临界区 [self.array addObject:@"data"]; // 解锁(计数+1,唤醒等待线程) dispatch_semaphore_signal(sema);
4. @synchronized(同步锁,最易用)
-
自旋 / 阻塞:纯阻塞
-
底层实现:Runtime 封装,底层关联 pthread 递归互斥锁
-
核心特性:可重入;无需手动初始化 / 销毁锁,直接传唯一对象(一般传 self 或自定义锁对象)即可,自动完成加锁 / 解锁;传 nil 会导致锁失效。
-
适用场景:快速开发、加锁场景简单的场景(如少量代码的多线程操作),无需关心底层细节。
-
OC 代码示例:
objc
// 传唯一锁对象,禁止传nil @synchronized (self) { // 临界区,自动加锁/解锁 [self.array addObject:@"data"]; }
5. NSLock(OC 面向对象互斥锁)
-
自旋 / 阻塞:纯阻塞
-
底层实现:Foundation 框架封装,基于 pthread 普通互斥锁
-
核心特性:OC 面向对象封装,不可重入;提供
lock/unlock/tryLock(尝试加锁,失败不阻塞)方法,比纯 C 的 pthread_mutex 易用。 -
适用场景:OC 开发中,想用地层锁的性能,又不想写纯 C 代码的轻量加锁场景。
-
OC 代码示例:
objc
NSLock *lock = [[NSLock alloc] init]; // 加锁 [lock lock]; // 临界区 [self.array addObject:@"data"]; // 解锁 [lock unlock]; // 尝试加锁 if ([lock tryLock]) { [lock unlock]; }
6. NSRecursiveLock(递归锁)
-
自旋 / 阻塞:纯阻塞
-
底层实现:Foundation 框架封装,基于 pthread 递归互斥锁
-
核心特性:可重入,解决 NSLock 的 “递归 / 嵌套加锁死锁” 问题;加锁次数 = 解锁次数才会释放锁。
-
适用场景:OC 开发中的递归调用、嵌套加锁场景。
-
OC 代码示例:
objc
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; - (void)recursiveMethod { [lock lock]; static int count = 0; if (count < 3) { count++; [self recursiveMethod]; } [lock unlock]; // 3次加锁,需3次解锁 }
7. NSCondition(条件锁)
-
自旋 / 阻塞:纯阻塞
-
底层实现:Foundation 框架封装,基于 pthread_mutex+pthread_cond(条件变量)
-
核心特性:可重入;不仅实现互斥,还支持条件等待(线程等待某个条件满足后再执行),典型的生产者 - 消费者模型实现方案;提供
wait(等待条件)、signal(唤醒一个等待线程)、broadcast(唤醒所有等待线程)方法。 -
适用场景:需要 “满足特定条件才执行” 的多线程场景(如生产者 - 消费者、线程间通信)。
-
OC 代码示例(生产者 - 消费者) :
objc
NSCondition *condition = [[NSCondition alloc] init]; NSMutableArray *goods = [NSMutableArray array]; // 消费者(等待生产) dispatch_async(dispatch_get_global_queue(0, 0), ^{ [condition lock]; while (goods.count == 0) { [condition wait]; } [goods removeLastObject]; [condition unlock]; }); // 生产者(生产后唤醒消费者) dispatch_async(dispatch_get_global_queue(0, 0), ^{ [condition lock]; [goods addObject:@"goods"]; [condition signal]; // 唤醒一个消费者 [condition unlock]; });
8. NSConditionLock(条件锁升级版)
-
自旋 / 阻塞:纯阻塞
-
底层实现:Foundation 框架封装,基于 NSCondition
-
核心特性:可重入;自带条件值,初始化指定默认条件值,加锁时可指定 “需要的条件值”,只有条件值匹配才会加锁成功,精准控制线程执行顺序。
-
适用场景:多线程需要按指定顺序执行的场景(如先执行线程 A,再执行线程 B,最后执行线程 C)。
-
OC 代码示例(指定执行顺序) :
objc
// 初始化默认条件值0 NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0]; // 线程C(条件值2才执行) dispatch_async(dispatch_get_global_queue(0, 0), ^{ [lock lockWhenCondition:2]; NSLog(@"执行线程C"); [lock unlockWithCondition:0]; }); // 线程B(条件值1才执行) dispatch_async(dispatch_get_global_queue(0, 0), ^{ [lock lockWhenCondition:1]; NSLog(@"执行线程B"); [lock unlockWithCondition:2]; }); // 线程A(默认条件值0,直接执行) dispatch_async(dispatch_get_global_queue(0, 0), ^{ [lock lock]; NSLog(@"执行线程A"); [lock unlockWithCondition:1]; }); // 执行结果:A→B→C
第三类:特殊锁 - 队列锁(GCD 锁,无自旋 / 阻塞,读多写少最优解)
队列锁并非传统意义上的 “锁”,无抢锁 / 等锁逻辑,不涉及自旋和阻塞,而是利用GCD 队列的任务调度特性实现线程安全,是 iOS 开发中读多写少场景的最优解,大厂开发首选方案。
核心原理
利用 GCD 队列的串行 / 并发特性,将共享资源操作的代码(临界区)扔到指定队列,通过队列的执行规则避免资源竞争:
- 串行队列:任务永远串行执行,同一时间只有一个任务操作资源,天然线程安全;
- 并发队列 + 屏障任务:读操作并行执行(提升性能),写操作通过屏障任务实现互斥(屏障任务会阻塞队列,单独执行,保证写操作的原子性)。
两种实现方式(开发必备)
方式 1:串行队列 + 同步派发(基础版,通用型)
-
核心特性:所有操作(读 + 写)同步派发到串行队列,天然串行执行,无需手动加解锁,死锁风险极低;不可重入(但实际开发中几乎无重入场景) 。
-
适用场景:读写操作频率相近的简单场景,追求开发效率。
-
OC 代码示例:
objc
// 初始化串行队列 @property (nonatomic, strong) dispatch_queue_t serialQueue; self.serialQueue = dispatch_queue_create("com.ios.lock.serial", DISPATCH_QUEUE_SERIAL); // 写操作 - (void)addData:(id)data { dispatch_sync(self.serialQueue, ^{ [self.array addObject:data]; }); } // 读操作 - (id)getDataAtIndex:(NSUInteger)index { __block id result; dispatch_sync(self.serialQueue, ^{ result = self.array[index]; }); return result; }
方式 2:并发队列 + 屏障任务(进阶版,读多写少最优解)
-
核心特性:读操作异步派发到并发队列,多线程并行执行(性能拉满);写操作通过屏障任务(dispatch_barrier_async) 实现互斥,屏障任务会阻塞队列,等所有前置读操作完成后单独执行,执行完再释放后续读操作,兼顾读性能和写安全。
-
适用场景:开发中最常见的读多写少场景(如列表数据读取、缓存操作、模型属性访问),大厂推荐最优写法。
-
关键注意:屏障任务仅对自定义并发队列有效,对 GCD 全局并发队列无效;读操作需返回结果时用
dispatch_sync,无需返回时用dispatch_async。 -
OC 代码示例:
objc
// 初始化自定义并发队列 @property (nonatomic, strong) dispatch_queue_t concurrentQueue; self.concurrentQueue = dispatch_queue_create("com.ios.lock.barrier", DISPATCH_QUEUE_CONCURRENT); // 读操作:异步并行,提升性能 - (id)getDataAtIndex:(NSUInteger)index { __block id result; dispatch_sync(self.concurrentQueue, ^{ result = self.array[index]; }); return result; } // 写操作:屏障异步,互斥执行 - (void)addData:(id)data { dispatch_barrier_async(self.concurrentQueue, ^{ [self.array addObject:data]; }); } -
Swift 代码示例:
swift
// 初始化自定义并发队列 private let concurrentQueue = DispatchQueue(label: "com.ios.lock.barrier", attributes: .concurrent) var array = [Any]() // 读操作 func getData(at index: Int) -> Any? { var result: Any? concurrentQueue.sync { guard index < array.count else { return } result = array[index] } return result } // 写操作:屏障任务 func addData(_ data: Any) { concurrentQueue.async(flags: .barrier) { self.array.append(data) } }
三、iOS 锁实战选型指南(开发直接套用)
选锁的核心原则:先看场景,再看性能,优先选择封装性好、死锁风险低的锁,以下选型顺序适配 99% 的 iOS 开发场景,从易用到高性能,从通用到特殊,直接套用即可。
- 读多写少场景:并发队列 + 屏障任务(队列锁进阶版)→ 大厂首选,兼顾读性能和写安全,无死锁风险;
- 读写频率相近 / 简单场景:串行队列 + 同步派发 / dispatch_semaphore_t(信号量锁) → 通用型,封装好,易上手;
- 快速开发 / 临时加锁: @synchronized → 最易用,无需手动管理锁,简单场景无脑用(注意不要传 nil);
- 临界区极短的高性能加锁:os_unfair_lock(iOS10+) → 混合自旋 + 阻塞,性能拉满,适配底层高性能需求;
- 递归 / 嵌套加锁场景:pthread_mutex_t(递归) / NSRecursiveLock / @synchronized → 选可重入锁,避免死锁;
- 条件等待 / 生产者 - 消费者场景:NSCondition → 基础条件锁,适配简单条件执行;
- 多线程指定顺序执行:NSConditionLock → 带条件值,精准控制线程执行顺序;
- 限制最大并发数:dispatch_semaphore_t → 计数 = N 即可实现,灵活高效。
四、开发避坑核心要点(必记)
- 不可重入锁禁止递归 / 嵌套加锁(如 os_unfair_lock、pthread_mutex 普通锁、NSLock),否则会直接死锁;
- @synchronized 禁止传 nil,传 nil 会导致锁失效,等同于未加锁;
- 自旋锁(os_unfair_lock)仅用在临界区极短的场景,临界区稍长会导致 CPU 空转、设备卡顿;
- 屏障任务仅对自定义并发队列有效,GCD 全局并发队列的屏障功能被苹果限制,无法实现互斥;
- 主队列禁止使用 dispatch_sync(同步派发) ,主队列为串行队列,同步派发会导致主线程阻塞死锁;
- iOS10 + 彻底放弃 OSSpinLock,统一用 os_unfair_lock 替代,避免优先级反转问题;
- 多线程写共享资源时,必须保证所有操作都加锁,部分加锁会导致数据错乱,前功尽弃。
五、总结
iOS 中的锁本质都是为了解决多线程资源竞争问题,核心差异在于自旋 / 阻塞特性、底层实现、封装程度:
- 自旋锁适合短临界区,但需注意使用场景,避免 CPU 空转;
- 阻塞锁是开发主力军,适配绝大多数场景,封装程度越高越易上手;
- 队列锁并非传统锁,利用 GCD 队列特性实现,是读多写少场景的最优解,也是大厂开发的首选方案。
开发中无需死记所有锁的底层实现,只需掌握自旋 / 阻塞的核心区别和实战选型指南,根据场景选择合适的锁,即可高效、安全地解决多线程问题。