在多线程开发中,经常会遇到多个操作同时访问同一个资源的情况,如果处理不好,很容易造成数据错乱和数据安全的问题。为了防止这个问题,就需要了解和引入线程锁的概念。
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 自旋锁
-
自旋锁的实现原理(深入理解iOS中的锁)
自旋锁的实现原理可以通过几个步骤来看,完全照抄参考文档:
首先,自旋锁存在的目的是为了确保临界区只有一个线程可以访问,也就是在第一个线程访问临界区时给它上锁,其他线程再来访问时就进入一个忙等状态。可以用如下伪代码表示:
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
它的使用基本过程就是初始化->加锁->执行操作->解锁,对应的方法如下:
// 初始化 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
基本方法使用:
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