前言
上一篇博客底层分析-锁我们主要探索了@synchronized底层实现原理,知道了这把锁为什么可以多线程递归加锁。同时也浅尝辄止了每把锁都是不同的,如果使用不好会造成死锁,下面继续探索锁的种类以及实现一把读写锁。
锁的归类
其实基本的锁就包括了三类 自旋锁、互斥锁、读写锁
,其他的比如条件锁,递归锁,信号量都是上层的封装和实现!
自旋锁
:线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等状态
。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了线程上下文的调度开销
,因此对于线程只会阻塞很短的场合是有效的。如OSSpinLock
、atomic
互斥锁
:防止多条线程对同一公共资源(比如全局变量)进行读写机制。该目的是通过将代码切片成一个个临界区而达成。其实简单的说同一时刻保证有一条线程执行任务,其他线程会处在睡眠状态。如NSLock
、pthread_mutex
、@synchronized
条件锁
:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。如NSCondition、NSConditionLock递归锁
:就是同一个线程可以加锁N次而不会引发死锁。如NSRecursiveLock、pthread_mutex信号量
:semaphore是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥读写锁
:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁
。
自旋锁和互斥锁异同
共同点
都能保证同一时刻只能有一个线程操作锁住的代码
不同点
- 互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入 睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
- 自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
- 自旋锁应用场景:比较适合做一些
不耗时的操作
锁的性能对比
通过开源框架LockPerformance对比锁的开锁和解锁的性能如下
OSSpinLock
(自旋锁) -> os_unfair_lock
(互斥锁) ->dispatch_semaphore_t
(信号量) -> pthread_mutex
(互斥锁) -> NSLock
(互斥锁) -> NSCondition
(条件锁) -> pthread_mutex_recursive
(互斥递归锁) -> NSRecursiveLock
(递归锁) -> NSConditionLock
(条件锁) -> synchronized
(互斥锁)
自旋锁线程安全吗?
@property (atomic, strong) NSArray *array;
//Thread A
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.array = @[@"Hank", @"CC", @"Cooci"];
}
else {
self.array = @[@"Kody"];
}
}
});
//Thread B
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (self.array.count >= 2) {
NSString* str = [self.array objectAtIndex:1];
}
}
});
分析:array是一个用自旋锁atomic
修饰的属性,在多线程
环境下会发生数组越界
的奔溃。线程B调用[self.array objectAtIndex:1]时很有可能线程A已经调用了self.array = @[@"Kody"],此时就会发生数组越界的奔溃,这里就需要使用读写锁了。
如何实现一个读写锁
首先分析一下读写锁的特性
- 多读单写:多条线程读,单条线程写
- 写入和写入互斥,不能同时写,在分析gcd的时候我们使用了栅栏函数保证写的唯一性
- 写和读互斥,写的时候不能读。
- 写不能堵塞主线程
- (void)viewDidLoad {
[super viewDidLoad];
self.dic=[[NSMutableDictionary alloc]init];
//自定义并发队列,栅栏函数必须自定义并发
queue=dispatch_queue_create("ttt", DISPATCH_QUEUE_CONCURRENT);
[self gy_safeSetter:@"gg"];
[self gy_safeSetter:@"bb"];
[self gy_safeSetter:@"tt"];
[self gy_safeSetter:@"mm"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//模拟多线程读
for(int i=0;i<20;i++){
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self gy_safeGetter];
});
}
}
//栅栏函数堵塞写,保证每次只有一个写入
-(void)gy_safeSetter:(NSString*)name{
__weak typeof(self) weakself=self;
dispatch_barrier_async(queue, ^{
[weakself.dic setValue:name forKey:@"gy"];
NSLog(@"写入成功%@",name);
});
}
//多线程多读,dispatch_sync堵塞的是当前线程,没法堵塞另外一条线程,如果换成dispatch_async,那么当前线程就是异步,result还没设置值就return了,为什么不直接 result=[weakself.dic objectForKey:@"gy"],为了读写互斥
-(NSString*)gy_safeGetter{
__weak typeof(self) weakself=self;
__block NSString* result;
dispatch_sync(queue, ^{
result=[weakself.dic objectForKey:@"gy"];
});
NSLog(@"%@",result);
return result;
}
分析:
- 首先看
gy_safeGetter
读的方法是如何实现多线程读
的。使用dispatch_sync
堵塞的是当前线程
,没法堵塞另外一条线程,如果换成dispatch_async
,那么当前线程就是异步,result还没设置值就return
了,为什么不直接result=[weakself.dic objectForKey:@"gy"]返回,那是为了保证在栅栏函数
中实现读写互斥
,栅栏函数的意义就是等待前面的事务完成再实现栅栏里面的事务。 - 然后看
gy_safeSetter
写方法,使用栅栏函数实现写写互斥
,保证只有一个在写,其他线程等待写完之后再写。注意栅栏函数必须是自定义的并发队列
,否则这个栅栏函数的作用等同于一个同步函数的作用