目录
一、资源竞争及加锁概述
二、三种加锁方式
1、@synchronized
2、NSLock
3、GCD信号量
一、资源竞争及加锁概述
通常情况下我们使用多线程开发往往是多个线程并发执行的,这时一个资源就可能被多个线程同时访问,造成资源竞争,这个过程中如果不对该资源的访问加一下锁,就会导致数据安全问题。
我们先举个“买票”的例子来说明多线程是如何导致资源竞争造成数据安全问题的。
@interface ViewController ()
@property (assign, atomic) int totalTicketCount;// 总票数
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 假设有两个窗口在卖票, 一共还剩下 6 张票, 第一个窗口排了 5 个人, 第二个窗口排了 4 个人
self.totalTicketCount = 6;
// 第一个窗口 --> 第一个线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i ++) {
[self buyTicket];
}
});
// 第二个窗口 --> 第二个线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 4; i ++) {
[self buyTicket];
}
});
}
- (void)buyTicket {
if (self.totalTicketCount <= 0) {
NSLog(@"%@===票没了, 明天再来吧", [NSThread currentThread]);
return;
}
NSLog(@"%@===剩余票数为 : %d", [NSThread currentThread], self.totalTicketCount);
self.totalTicketCount --;// 卖一张
}
输出 :
<NSThread: 0x604000467b80>{number = 4, name = (null)}===剩余票数为 : 6
<NSThread: 0x6000004766c0>{number = 3, name = (null)}===剩余票数为 : 6
<NSThread: 0x6000004766c0>{number = 3, name = (null)}===剩余票数为 : 4
<NSThread: 0x604000467b80>{number = 4, name = (null)}===剩余票数为 : 5
<NSThread: 0x604000467b80>{number = 4, name = (null)}===剩余票数为 : 2
<NSThread: 0x6000004766c0>{number = 3, name = (null)}===剩余票数为 : 2
<NSThread: 0x604000467b80>{number = 4, name = (null)}===剩余票数为 : 1
<NSThread: 0x6000004766c0>{number = 3, name = (null)}===票没了, 明天再来吧
<NSThread: 0x6000004766c0>{number = 3, name = (null)}===票没了, 明天再来吧
可见:
数据出问题了。
分析一下:
比如现在有6张票,如果不给资源加锁,那么两个线程是都可以进来访问这个票数的,假设第一个线程先进来了,判断完之后它在做减1操作但是还没来得及减,第二个线程就进来做判断了,那么因为第一个线程还没有完成减1操作,所以第二个线程做判断的时候用的还是6做判断,但其实第一个线程已经在作减1操作,第二个线程应该是用5来做判断的,就是线程一没减完线程二就进来了,所以造成了数据出问题。
因此,为了避免这种多个线程资源竞争导致的数据安全问题,我们就需要给资源访问代码加锁来保证同一时间只有一个线程能访问资源。iOS中我们常用的资源加锁方式有三种:@synchronized, NSLock和GCD信号量
。
二、三种加锁方式
1、@synchronized
使用方法:@synchronized(self) {加锁代码}
我们将上面的buyTicket方法修改一下:
- (void)buyTicket {
@synchronized(self) {
if (self.totalTicketCount <= 0) {
NSLog(@"%@===票没了, 明天再来吧", [NSThread currentThread]);
return;
}
NSLog(@"%@===剩余票数为 : %d", [NSThread currentThread], self.totalTicketCount);
self.totalTicketCount --;// 卖一张
}
}
2、NSLock
使用方法:[self.lock lock]; {加锁代码} [self.lock unlock];
我们将上面的buyTicket方法修改一下(自己创建一个lock属性):
- (void)buyTicket {
[self.lock lock];
if (self.totalTicketCount <= 0) {
NSLog(@"%@===票没了, 明天再来吧", [NSThread currentThread]);
return;
}
NSLog(@"%@===剩余票数为 : %d", [NSThread currentThread], self.totalTicketCount);
self.totalTicketCount --;// 卖一张
[self.lock unlock];
}
3、GCD信号量
使用GCD信号量实现加锁的原理:主要是利用信号的通知信号和等待信号来实现的,即每当我们发送一个通知信号时,信号的信号量会+1;每当我们发送一个等待信号时,信号的信号量会-1;而如果信号的信号量为0,这个信号会进入等待状态,使得其它线程无法执行,直到等待过程中我们发出了通知信号使得信号的信号量+1了,其它线程才能继续执行。因此利用这个原理我们可以初始化一个GCD信号并且默认信号量为1,在加锁代码前发送一个等待信号,让信号量-1减为0,信号开始等待,使得其它线程处于等待状态无法进入,执行完后再发送一个通知信号,让信号量+1加为1,让其他线程释放等待,继续执行加锁代码。
我们可以修改代码为:
@interface ViewController ()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;// GCD 信号量
@property (assign, atomic) int totalTicketCount;// 总票数
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// GCD 信号量, 并设置信号量的默认值为 1
self.semaphore = dispatch_semaphore_create(1);
// 假设有两个窗口在卖票, 一共还剩下 6 张票, 第一个窗口排了 5 个人, 第二个窗口排了 4 个人
self.totalTicketCount = 6;
// 第一个窗口 --> 第一个线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i ++) {
[self buyTicket];
}
});
// 第二个窗口 --> 第二个线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 4; i ++) {
[self buyTicket];
}
});
}
- (void)buyTicket {
// 发送等待信号, 发现信号量为 1, 执行下面的代码, 并将信号量 -1, 减为 0 --> 信号进入等待状态, 其它线程无法进入
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
if (self.totalTicketCount <= 0) {
NSLog(@"%@===票没了, 明天再来吧", [NSThread currentThread]);
return;
}
NSLog(@"%@===剩余票数为 : %d", [NSThread currentThread], self.totalTicketCount);
self.totalTicketCount --;// 卖一张
// 发送通知信号, 信号量 +1, 加为 1 --> 信号释放等待状态, 其它线程继续执行加锁代码
dispatch_semaphore_signal(self.semaphore);
}
@end
4、三种加锁方式的对比
@synchronized、NSLock和使用GCD信号实现资源加锁这三种方式在使用便捷度上依次递减的,但是性能却是依次递增的。
5、给资源加锁时候我们需要注意什么
-
给资源加锁是因为有的场景确实需要通过资源加锁来保证数据安全,但并不是什么情况都要加锁的,因为加锁会保证只有一个线程在执行,这就失去了当初开辟多线程的意义。
-
资源加锁的最终原理都是使得一个线程在访问某个资源时,其它的线程被阻塞,直到前一个线程访问完,后一个线程的阻塞才会被释放从而访问资源。因此加锁代码应该仅仅是对被抢占资源的读取或者修改的代码,而不应该无脑地将过多的代码放到锁里面,否则一个线程执行的时候另一个线程就得等很长时间,等都等死了。
-
我们虽然通过加锁在这个地方(同一个地方)保证了多个线程不能通过这段代码同时访问资源,但是我们不能保证别的地方没有别的代码在读取或者修改资源呀,因此我们最好是把资源用atomic来修饰,因为atomic执行读取的是寄存器的数据,而nonatomic读取的是内存数据,这样一来就可以保证不同地方的多个线程也可以访问到正确的资源。
网友评论