iOS多线程(五):线程锁

在多线程开发中,经常会遇到多个操作同时访问同一个资源的情况,如果处理不好,很容易造成数据错乱和数据安全的问题。为了防止这个问题,就需要了解和引入线程锁的概念。

1、基础概念

锁是一种同步机制,用于多线程环境中对资源访问的限制,可以理解为锁是用于排除并发的一种机制。

来举个简单的的例子,刚刚我们产品经理请喝奶茶,那么就以美团买奶茶为例吧。“一点点”剩余的阿萨姆奶茶总共有30份,在接下来的5分钟内有两个公司的员工都在看同一家店,而且同时分别有10个人去点阿萨姆奶茶。如果这个剩余的数量没有做任何处理的情况,在上面说的同时(注意这里时间点非常接近)的情况下,剩余数量一定会出现错乱。来看看代码:


// 买奶茶实例测试
- (void)buyMilkTeaTest
{
    self.totalMilkTeas = 30;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        do {
            [self buyAMilkTea];
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 30);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        do {
            [self buyAMilkTea];
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 30);
    });
}

// 买奶茶操作
- (void)buyAMilkTea
{
    NSInteger currentNum = self.totalMilkTeas;
    sleep(0.2);
    currentNum -= 1;
    self.totalMilkTeas = currentNum;
    NSLog(@"剩余奶茶数 - %ld", currentNum);
}

那么输出结果一般是这样的,剩余奶茶数出现混乱,这样会导致社会问题的。

2019-07-20 09:44:20.383381+0800 Thread-Test[15607:446826] 剩余奶茶数 - 29
2019-07-20 09:44:20.383381+0800 Thread-Test[15607:446820] 剩余奶茶数 - 29
2019-07-20 09:44:20.383494+0800 Thread-Test[15607:446820] 剩余奶茶数 - 28
2019-07-20 09:44:20.383494+0800 Thread-Test[15607:446826] 剩余奶茶数 - 28
2019-07-20 09:44:20.383626+0800 Thread-Test[15607:446826] 剩余奶茶数 - 27
2019-07-20 09:44:20.383626+0800 Thread-Test[15607:446820] 剩余奶茶数 - 27
2019-07-20 09:44:20.383753+0800 Thread-Test[15607:446820] 剩余奶茶数 - 26
2019-07-20 09:44:20.384242+0800 Thread-Test[15607:446826] 剩余奶茶数 - 25
2019-07-20 09:44:20.384449+0800 Thread-Test[15607:446820] 剩余奶茶数 - 24
2019-07-20 09:44:20.385961+0800 Thread-Test[15607:446820] 剩余奶茶数 - 23
2019-07-20 09:44:20.387045+0800 Thread-Test[15607:446826] 剩余奶茶数 - 22
2019-07-20 09:44:20.387431+0800 Thread-Test[15607:446826] 剩余奶茶数 - 21
2019-07-20 09:44:20.387521+0800 Thread-Test[15607:446820] 剩余奶茶数 - 21
2019-07-20 09:44:20.387681+0800 Thread-Test[15607:446826] 剩余奶茶数 - 20
2019-07-20 09:44:20.387751+0800 Thread-Test[15607:446820] 剩余奶茶数 - 19
2019-07-20 09:44:20.387901+0800 Thread-Test[15607:446826] 剩余奶茶数 - 18
2019-07-20 09:44:20.388406+0800 Thread-Test[15607:446820] 剩余奶茶数 - 17
2019-07-20 09:44:20.388650+0800 Thread-Test[15607:446826] 剩余奶茶数 - 16
2019-07-20 09:44:20.388938+0800 Thread-Test[15607:446820] 剩余奶茶数 - 15
2019-07-20 09:44:20.389198+0800 Thread-Test[15607:446826] 剩余奶茶数 - 14
2019-07-20 09:44:20.389387+0800 Thread-Test[15607:446820] 剩余奶茶数 - 13
2019-07-20 09:44:20.389615+0800 Thread-Test[15607:446826] 剩余奶茶数 - 12
2019-07-20 09:44:20.389803+0800 Thread-Test[15607:446820] 剩余奶茶数 - 11
2019-07-20 09:44:20.398285+0800 Thread-Test[15607:446826] 剩余奶茶数 - 10
2019-07-20 09:44:20.398717+0800 Thread-Test[15607:446820] 剩余奶茶数 - 9
2019-07-20 09:44:20.398814+0800 Thread-Test[15607:446826] 剩余奶茶数 - 8
2019-07-20 09:44:20.398973+0800 Thread-Test[15607:446820] 剩余奶茶数 - 7
2019-07-20 09:44:20.399076+0800 Thread-Test[15607:446826] 剩余奶茶数 - 6
2019-07-20 09:44:20.399285+0800 Thread-Test[15607:446820] 剩余奶茶数 - 5
2019-07-20 09:44:20.399919+0800 Thread-Test[15607:446826] 剩余奶茶数 - 4
2019-07-20 09:44:20.400857+0800 Thread-Test[15607:446826] 剩余奶茶数 - 2
2019-07-20 09:44:20.400585+0800 Thread-Test[15607:446820] 剩余奶茶数 - 3
2019-07-20 09:44:20.401007+0800 Thread-Test[15607:446826] 剩余奶茶数 - 1
2019-07-20 09:44:20.401007+0800 Thread-Test[15607:446820] 剩余奶茶数 - 1
2019-07-20 09:44:20.401649+0800 Thread-Test[15607:446826] 剩余奶茶数 - 0

所以,对于剩余奶茶的数量在这里是属于线程不安全的,需要想办法让这个剩余数量一次只能一个线程访问,锁就出现了。

2、锁的分类

在iOS中,锁可以分为互斥锁、递归锁、信号量、条件锁、自旋锁、读写锁(一种特殊的自旋锁)、分布式锁等。


锁的分类

2.1 OSSpinLock 自旋锁

自旋锁的实现原理可以通过几个步骤来看,完全照抄参考文档:

首先,自旋锁存在的目的是为了确保临界区只有一个线程可以访问,也就是在第一个线程访问临界区时给它上锁,其他线程再来访问时就进入一个忙等状态。可以用如下伪代码表示:

do {  
    Acquire Lock
        Critical section  // 临界区
    Release Lock
        Reminder section // 不需要锁保护的代码
}

Acquire Lock这步就是为了给临界区加锁,此时可以通过定义一个全局的变量,用来记录当前锁是否可用,那么伪代码如下:

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {  
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

可惜这段代码存在一个问题: 如果一开始有多个线程同时执行 while 循环,他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了。解决思路也很简单,只要确保申请锁的过程是原子操作即可。

狭义上的原子操作表示一条不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。在单处理器环境下,一条汇编指令显然是原子操作,因为中断也要通过指令来实现。

然而在多处理器的情况下,能够被多个处理器同时执行的操作仍然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。

这些非常底层的概念无需完全掌握,我们只要知道上述申请锁的过程,可以用一个原子性操作 test_and_set 来完成,它用伪代码可以这样表示:

bool test_and_set (bool *target) {  
    bool rv = *target; 
    *target = TRUE; 
    return rv;
}

如果临界区的执行时间过长,使用自旋锁不是个好主意。根据时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的。

上面就是大神谈到的自旋锁的底层实现原理,反正过程就是我们从自己的代码开始,去一步一步实现底层硬件的原子属性,原子属性就可以理解为是单条线路线程安全的意思。

  • OSSpinLock的使用

接下来看一下OSSpinLock的使用过程
首先需要导入头文件

#import <libkern/OSAtomic.h>

它的使用基本过程就是初始化->加锁->执行操作->解锁,对应的方法如下:

// 初始化 OS_SPINLOCK_INIT默认值为 0,在 locked 状态时就会大于 0,unlocked状态下为 0
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
//你需要保护的操作
    {}
// 解锁
OSSpinLockUnlock(&spinLock);

OSSpinLock在各种锁中,性能是最好的,但是由于它存在优先级反转的问题,苹果在iOS10以后也已经弃用了,具体的原因可以参考大神的文章:不再安全的 OSSpinLock,几乎被所有讲iOS线程锁的文章引用。

接下来我们回到代码上,还是文章开头买奶茶的例子,我们用OSSpinLock加锁后再来看看输出结果。

  __block OSSpinLock spinLock = OS_SPINLOCK_INIT; // 初始化
    self.totalMilkTeas = 20;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        do {
            OSSpinLockLock(&spinLock); // 加锁
            [self buyAMilkTea];
            OSSpinLockUnlock(&spinLock); // 解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 30);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        do {
            OSSpinLockLock(&spinLock);
            [self buyAMilkTea];
            OSSpinLockUnlock(&spinLock);
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 30);
    });

输出结果,运行5次,没有出现总数混乱:

2019-07-20 10:11:43.401546+0800 Thread-Test[15843:456439] 剩余奶茶数 - 19
2019-07-20 10:11:43.401904+0800 Thread-Test[15843:456439] 剩余奶茶数 - 18
2019-07-20 10:11:43.402135+0800 Thread-Test[15843:456439] 剩余奶茶数 - 17
2019-07-20 10:11:43.402373+0800 Thread-Test[15843:456439] 剩余奶茶数 - 16
2019-07-20 10:11:43.402896+0800 Thread-Test[15843:456439] 剩余奶茶数 - 15
2019-07-20 10:11:43.403151+0800 Thread-Test[15843:456439] 剩余奶茶数 - 14
2019-07-20 10:11:43.403290+0800 Thread-Test[15843:456439] 剩余奶茶数 - 13
2019-07-20 10:11:43.403790+0800 Thread-Test[15843:456439] 剩余奶茶数 - 12
2019-07-20 10:11:43.404095+0800 Thread-Test[15843:456439] 剩余奶茶数 - 11
2019-07-20 10:11:43.404417+0800 Thread-Test[15843:456439] 剩余奶茶数 - 10
2019-07-20 10:11:43.404889+0800 Thread-Test[15843:456439] 剩余奶茶数 - 9
2019-07-20 10:11:43.405202+0800 Thread-Test[15843:456439] 剩余奶茶数 - 8
2019-07-20 10:11:43.405319+0800 Thread-Test[15843:456439] 剩余奶茶数 - 7
2019-07-20 10:11:43.405628+0800 Thread-Test[15843:456439] 剩余奶茶数 - 6
2019-07-20 10:11:43.405876+0800 Thread-Test[15843:456439] 剩余奶茶数 - 5
2019-07-20 10:11:43.406908+0800 Thread-Test[15843:456439] 剩余奶茶数 - 4
2019-07-20 10:11:43.407001+0800 Thread-Test[15843:456439] 剩余奶茶数 - 3
2019-07-20 10:11:43.410416+0800 Thread-Test[15843:456439] 剩余奶茶数 - 2
2019-07-20 10:11:43.410667+0800 Thread-Test[15843:456439] 剩余奶茶数 - 1
2019-07-20 10:11:43.414773+0800 Thread-Test[15843:456443] 剩余奶茶数 - 0

2.2 信号量dispatch_semaphore_t

信号量之前讲GCD的时候也讲过,通过设置信号的值可以有不同的用途。信号量主要通过三个方法来控制,作为锁,主要用到的是当信号值为0时它会阻塞线程这一特性,它是一种互斥锁。

互斥锁:如果共享数据已经有其他线程加锁了,第二个线程再去访问时会进入休眠状态等待资源被解锁。一旦被访问的资源被解锁, 则第二个等待资源的线程会被唤醒。

三个方法说明:

  • dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句。
  • dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1。
  • dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal 值 +1。

还是同样的例子,通过信号量加锁来实现的代码:

 dispatch_semaphore_t lock = dispatch_semaphore_create(1); // 信号值为0时阻塞线程
    
    self.totalMilkTeas = 20;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        do {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); // 信号量-1,此时信号量变为0,阻塞线程,加锁
            [self buyAMilkTea];
            dispatch_semaphore_signal(lock); // 信号量+1,解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        do {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); // 信号量-1,此时信号量变为0,阻塞线程,加锁
            [self buyAMilkTea];
            dispatch_semaphore_signal(lock); // 信号量+1,解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
    });

2.3 pthread_mutex

pthread_mutex是基于C语言的一套接口,之前讲pthread的时候有讲过iOS多线程中的几种实现方式,最终其实都是通过pThread实现的,而pthread_mutex从它的名字就知道是互斥锁。

在OSSpinLock被废弃后,pthread_mutex由于性能相较于其他的锁来说要好,所以被用得比较多,比如苹果的RunLoop源码中就有它的身影,还有很多其他的第三方库。

使用pthread_mutex需要导入头文件:

#import <pthread.h>

基本方法使用:

self.totalMilkTeas = 20;
    static pthread_mutex_t pLock;
    pthread_mutex_init(&pLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        do {
            pthread_mutex_lock(&pLock); // 加锁
            [self buyAMilkTea];
            pthread_mutex_unlock(&pLock); // 解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        do {
            pthread_mutex_lock(&pLock); // 加锁
            [self buyAMilkTea];
            pthread_mutex_unlock(&pLock); // 解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
    });

由pthread_mutex还衍生了一种递归锁,如上面的示例,如果用 pthread_mutex_lock 先锁上了,但未执行解锁的时候,再次上锁,就会导致死锁的情况。递归锁的出现就是为了避免死锁的情况发生。

实现递归锁,需要设置锁的类型:

pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr); // 初始化attr并且给它赋予默认
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 设置锁类型,这边是设置为递归锁
    pthread_mutex_init(&pLock, &attr);
    pthread_mutexattr_destroy(&attr); // 销毁一个属性对象,在重新进行初始化之前该结构不能重新使用

pthread_mutex所得类型如下:

#define PTHREAD_MUTEX_NORMAL        0 //  默认类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁,不允许重复加锁。
#define PTHREAD_MUTEX_ERRORCHECK    1 // 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
#define PTHREAD_MUTEX_RECURSIVE     2 // 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

2.4 NSLock系列

来看一下苹果封装在NSFoundation中的几个线程锁:NSLock 、NSCondition 、 NSConditionLock 、 NSRecursiveLock。这四个锁都定义在NSLock.h文件中,都遵循如下协议,使用方法也类似:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end
  • NSLock

NSLock是最简单的普通对象锁,它底层是对 pthread_mutex_t 的封装,对应的参数是PTHREAD_MUTEX_NORMAL,属于互斥锁。除了lock/unlock/trylock这几个常规方法外,NSLock还要一个特殊方法:

//这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回 YES,反之返回 NO
- (BOOL)lockBeforeDate:(NSDate *)limit;  

NSLock基本使用:

self.totalMilkTeas = 20;
    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        do {
            [lock lock]; // 加锁
            [self buyAMilkTea];
            [lock unlock]; // 解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        do {
            [lock lock]; // 加锁
            [self buyAMilkTea];
            [lock unlock]; // 解锁
        } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
    });
  • NSCondition

NSCondition 底层则是对 pthread_cond_t 的封装,属于条件锁,除了lock和unlock方法,它还有如下几个方法:

- (void)wait; // 线程进入等待状态
- (BOOL)waitUntilDate:(NSDate *)limit; // 线程等待指定时间
- (void)signal; // 唤醒一个线程
- (void)broadcast; // 唤醒所有线程

NSCondition 的对象实际是一个线程检查器,一个线程上锁后,其它线程也能上锁,而之后可以根据条件决定是否继续运行线程。所谓的条件是由等待状态决定的,只有当遇到signal方法或者broadcast方法,线程被唤醒后,才会继续运行之后的方法。

有一个实例,三个线程上锁后都被wait了:

NSCondition *lock = [NSCondition new];
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程1加锁成功");
        [lock wait];
        NSLog(@"线程1");
        [lock unlock];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程2加锁成功");
        [lock wait];
        NSLog(@"线程2");
        [lock unlock];
    });
    
    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程3加锁成功");
        [lock wait];
        NSLog(@"线程3");
        [lock unlock];
    });

此时三个线程中的后续代码都不会执行,都被阻塞在了wait那里。

2019-07-20 14:00:12.031320+0800 Thread-Test[17469:545640] 线程1加锁成功
2019-07-20 14:00:12.031509+0800 Thread-Test[17469:545626] 线程2加锁成功
2019-07-20 14:00:12.031658+0800 Thread-Test[17469:545627] 线程3加锁成功

那么对如上代码稍作修改,把线程3的wait注释掉,加一个唤醒线程的signal,注意这里只会唤醒一个线程:

    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1); // 为了确保线程3的操作在最后执行
        [lock lock];
        NSLog(@"线程3加锁成功");
//        [lock wait];
        NSLog(@"线程3");
        [lock unlock];
        
        [lock signal];
//        [lock broadcast];
    });

输出结果:

2019-07-20 14:21:02.699761+0800 Thread-Test[17679:553564] 线程1加锁成功
2019-07-20 14:21:02.700209+0800 Thread-Test[17679:553562] 线程2加锁成功
2019-07-20 14:21:03.702595+0800 Thread-Test[17679:553563] 线程3加锁成功
2019-07-20 14:21:03.702830+0800 Thread-Test[17679:553563] 线程3
2019-07-20 14:21:03.703099+0800 Thread-Test[17679:553564] 线程1

如果使用broadcast唤醒所有的线程,那么线程1和线程2都会被唤醒,并执行其各自的后续操作。

2019-07-20 14:23:59.398866+0800 Thread-Test[17710:554910] 线程1加锁成功
2019-07-20 14:23:59.399108+0800 Thread-Test[17710:554909] 线程2加锁成功
2019-07-20 14:24:00.399339+0800 Thread-Test[17710:554917] 线程3加锁成功
2019-07-20 14:24:00.399620+0800 Thread-Test[17710:554917] 线程3
2019-07-20 14:24:00.399863+0800 Thread-Test[17710:554910] 线程1
2019-07-20 14:24:00.400064+0800 Thread-Test[17710:554909] 线程2
  • NSConditionLock

NSConditionLock 的底层则是使 NSCondition 实现的。
NSConditionLock比NSLock多了一个 condition 属性,而且可以发现每个方法几乎都多了一个关于 condition 属性的方法。

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

lockWhenCondition:方法,只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。而 unlockWithCondition: 并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值。

 // 初始化条件为0
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
    
    // 线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lockWhenCondition:1]; // 初始不满足条件,上锁失败
        NSLog(@"线程1");
        sleep(2);
        [lock unlock];
    });
    
    // 线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1); // 以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:0]) { // 初始条件为0,满足上锁条件,尝试上锁成功
            NSLog(@"线程2");
            [lock unlockWithCondition:2]; // 解锁后上锁条件变为了2
            NSLog(@"线程2解锁成功");
        } else {
            NSLog(@"线程2尝试加锁失败");
        }
    });
    
    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) { // 在这段代码后于线程2代码段执行的前提下,执行到这里时,condition已经变为了2
            NSLog(@"线程3");
            [lock unlock];
            NSLog(@"线程3解锁成功");
        } else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
    
    //线程4
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(3);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程4");
            [lock unlockWithCondition:1];
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });
  • 运行到线程1时,由于上锁条件是0,而这里的判断条件是1,不满足上锁要求,线程1被阻塞。

  • 运行到线程2时,由于初始条件为0,满足上锁条件,线程2上锁成功;线程2解锁时把上锁条件重置为2。

  • 执行到线程3的时候,上锁条件已经被线程2修改为2,此时线程3也上锁成功。

  • 执行到线程4的时候,上锁条件仍然为2,线程4上锁成功。线程4解锁,并把上锁条件重置为1,此时线程1的上锁条件满足,阻塞被解除,继续执行下面的操作。

  • NSRecursiveLock

NSRecursiveLock 则是对 pthread_mutex_t 的PTHREAD_MUTEX_RECURSIVE 参数的封装。
NSRecursiveLock 是递归锁,它可以在一个线程中重复加锁,它 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

具体什么用途暂时还没弄明白,后续再补上吧。

2.5 @synchronized

@synchronized应该非常熟悉,一言不合要加锁就用@synchronized,可是从来没注意原来它是所有线程锁中性能最差的一个,它属于互斥锁。
@synchronized后面待了一个参数,传一个对象。@synchronized其实是在之前讲到的几种锁的基础上锁了多重的封装,就像一张火车票经过多个黄牛一层一层转卖,可想而知效率自然要低。

还是之前的实例,用@sychronized()来加锁:

self.totalMilkTeas = 20;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1");
        @synchronized (self) {
            do {
                [self buyAMilkTea];
            } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
        }
        
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2");
        @synchronized (self) {
            do {
                [self buyAMilkTea];
            } while (self.totalMilkTeas > 1 && self.totalMilkTeas <= 20);
        }
    });

综上所述,有这么多线程锁,还怕线程不安全吗。关于各种线程锁的性能,和怎么选择线程锁,可以参考不再安全的OSSPinLock

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342