iOS 开发11 种锁全解析

9 阅读13分钟

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 开发场景,从易用到高性能,从通用到特殊,直接套用即可。

  1. 读多写少场景并发队列 + 屏障任务(队列锁进阶版)→ 大厂首选,兼顾读性能和写安全,无死锁风险;
  2. 读写频率相近 / 简单场景串行队列 + 同步派发 / dispatch_semaphore_t(信号量锁) → 通用型,封装好,易上手;
  3. 快速开发 / 临时加锁@synchronized → 最易用,无需手动管理锁,简单场景无脑用(注意不要传 nil);
  4. 临界区极短的高性能加锁os_unfair_lock(iOS10+) → 混合自旋 + 阻塞,性能拉满,适配底层高性能需求;
  5. 递归 / 嵌套加锁场景pthread_mutex_t(递归) / NSRecursiveLock / @synchronized → 选可重入锁,避免死锁;
  6. 条件等待 / 生产者 - 消费者场景NSCondition → 基础条件锁,适配简单条件执行;
  7. 多线程指定顺序执行NSConditionLock → 带条件值,精准控制线程执行顺序;
  8. 限制最大并发数dispatch_semaphore_t → 计数 = N 即可实现,灵活高效。

四、开发避坑核心要点(必记)

  1. 不可重入锁禁止递归 / 嵌套加锁(如 os_unfair_lock、pthread_mutex 普通锁、NSLock),否则会直接死锁;
  2. @synchronized 禁止传 nil,传 nil 会导致锁失效,等同于未加锁;
  3. 自旋锁(os_unfair_lock)仅用在临界区极短的场景,临界区稍长会导致 CPU 空转、设备卡顿;
  4. 屏障任务仅对自定义并发队列有效,GCD 全局并发队列的屏障功能被苹果限制,无法实现互斥;
  5. 主队列禁止使用 dispatch_sync(同步派发) ,主队列为串行队列,同步派发会导致主线程阻塞死锁;
  6. iOS10 + 彻底放弃 OSSpinLock,统一用 os_unfair_lock 替代,避免优先级反转问题;
  7. 多线程写共享资源时,必须保证所有操作都加锁,部分加锁会导致数据错乱,前功尽弃。

五、总结

iOS 中的锁本质都是为了解决多线程资源竞争问题,核心差异在于自旋 / 阻塞特性、底层实现、封装程度

  • 自旋锁适合短临界区,但需注意使用场景,避免 CPU 空转;
  • 阻塞锁是开发主力军,适配绝大多数场景,封装程度越高越易上手;
  • 队列锁并非传统锁,利用 GCD 队列特性实现,是读多写少场景的最优解,也是大厂开发的首选方案。

开发中无需死记所有锁的底层实现,只需掌握自旋 / 阻塞的核心区别实战选型指南,根据场景选择合适的锁,即可高效、安全地解决多线程问题。