前言
锁是为了解决多线程的安全问题。
如下实例:
count在线程中顺序输出10次,但是并没有顺序输出;
线程安全
多线程操作共享数据的时候,不会出现意想不到的结果,就是线程安全,否则就是线程不安全。
原子属性能否保证线程安全?
查看get和set的部分源码:
原⼦属性只能保障
set或者get的读写安全,但我们在使⽤属性的时候,往往既有set⼜有get,所以说原⼦属性并不是线程安全的。
自旋锁和互斥锁
- ⾃旋锁: 在访问被锁的资源的时候,调⽤者线程不会休眠,⽽是不停循环在那⾥,直到被锁资源释放锁。(忙等)
- 互斥锁: 在访问被锁资源时,调⽤者线程会休眠,此时cpu可以调度其他线程⼯作。直到被锁的资源释放锁。然后再唤醒休眠线程。(闲等)
自旋锁优缺点
- ⾃旋锁的优点在于,因为⾃旋锁不会引起调⽤者线程休眠,所以不会进⾏线程调度,cpu时间⽚轮转等⼀些耗时的操作。所以如果能在很短的时间内获得锁,⾃旋锁的效率远⾼于互斥锁。
- ⾃旋锁缺点在于,⾃旋锁⼀直占⽤CPU,在未获得锁的情况下,⼀直⾃旋,相当于死循环,会⼀直占⽤着CPU,如果不能在很短的时间内获得锁,这⽆疑会使CPU效率降低。 ⽽且⾃旋锁不能实现递归调⽤。
⾃旋锁优先级反转的bug
当多个线程有优先级的时候,如果⼀个优先级低的线程先去访问某个数据,此时使⽤⾃旋锁进⾏了加锁,然后⼀个优先级⾼的线程⼜去访问这个数据,那么优先级⾼的线程因为优先级⾼会⼀直占着CPU资源,此时优先级低的线程⽆法与优先级⾼的线程争夺 CPU 时间,从⽽导致任务迟迟完不成、锁⽆法释放。
OSSpinLock
- 是一种自旋锁,iOS10之后被移除。
- OS_SPINLOCK_INIT 初始化锁 。
- OSSpinLockLock(&spinlock) 加锁,参数为OSSPINLOCK地址。
- OSSpinLockUnlock(&spinlock) 解锁,参数是OSSpinLock地址。
- OSSpinLockTry(&spinlock) 尝试上锁,参数是OSSpinLock地址。如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功。
- 由于⾃旋锁本身存在的这个问题,所以苹果在iOS10以后已经废弃了OSSpinLock。
- 也就是说除⾮⼤家能保证访问锁的线程全部都处于同⼀优先级,否则 iOS 系统中的⾃旋锁就不要去使⽤了。
os_unfair_lock
- iOS10之后开始支持,用于取代OSSpinLock。
- OS_UNFAIR_LOCK_INIT 初始化锁。
- os_unfair_lock_lock 加锁。参数为os_unfair_lock地址。
- os_unfair_lock_unlock 解锁。参数为os_unfair_lock地址。
- os_unfair_lock_trylock 尝试加锁。参数为os_unfair_lock地址。如果成功返回true。如果锁已经被锁定则返回false。
- os_unfair_lock_assert_owner 参数为os_unfair_lock地址。如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。
- os_unfair_lock_assert_not_owner 参数为os_unfair_lock地址。如果当前线程持有指定的锁,则触发崩溃
self.unfailLock = OS_UNFAIR_LOCK_INIT;
- (void)testOsUnfairLock{
os_unfair_lock_lock(&_unfailLock);
self.count --;
NSLog(@"%ld",self.count);
os_unfair_lock_unlock(&_unfailLock);
}
NSLock
- -(void)lock 加锁。
- -(void)unlock 解锁。
- -(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。
- -(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。
- @property (nullable ,copy) NSString *name 锁名称。
self.lock = [[NSLock alloc] init];
self.lock.name = @"name";
- (void)test_lock{
[self.lock lock];
self.count --;
NSLog(@"%ld",self.count);
[self.lock unlock];
}
NSRecursiveLock
- -(void)lock 加锁。
- -(void)unlock 解锁。
- -(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。
- -(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。
- @property (nullable ,copy) NSString *name 锁名称。
-(void)test_recursiveLock {
[self.recursiveLock lock];
self.count --;
NSLog(@"%ld",self.count);
[self.recursiveLock unlock];
}
NSRecursiveLock 是递归锁,可以递归调用,但是只能在同一个线程递归调用,不能被多条线程所拥有;
NSCondition
- -(void)lock 加锁。
- -(void)unlock 解锁。
- -(void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
- -(void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
- -(void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
- -(void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。
- @property (nullable ,copy) NSString *name 锁名称。
生产者消费者实例
- (void)test_nscondition{
//生产
for (NSInteger i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test_production];
});
}
for (NSInteger i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test_consumption];
});
}
}
- (void)test_production {
[self.condition lock];
self.count ++;
NSLog(@"生产了一个产品,现有产品 : %ld个",(long)self.count);
[self.condition signal];
[self.condition unlock];
}
- (void)test_consumption {
[self.condition lock];
//这里如果改成if会因为虚假唤醒,出现负数
while ( self.count == 0 ) {
[self.condition wait];
}
self.count --;
NSLog(@"消费了一个产品,现有产品: %ld个",(long)self.count);
[self.condition unlock];
}
NSCondition存在虚假唤醒
- 当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满⾜时,就会发⽣虚假唤醒。之所以称为虚假,是因为该线程似乎⽆缘⽆故地被唤醒了。但是虚假唤醒不会⽆缘⽆故发⽣:它们通常是因为在发出条件变量信号和等待线程最终运⾏之间,另⼀个线程运⾏并更改了条件。线程之间存在竞争条件,典型的结果是有时,在条件变量上唤醒的线程⾸先运⾏,赢得竞争,有时它运⾏第⼆,失去竞争。
- 在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变量发出信号时等待它,系统可能会决定将它们全部唤醒,将每个signal( )唤醒⼀个线程视为broadcast( )唤醒所有这些,从⽽打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线程在等待,那么只有⼀个会获胜,另外 9 个会经历虚假唤醒。
NSConditionLock
- 基于NSCondition
- -(void)lock 加锁。
- -(void)unlock 解锁。
- -(instancetype)initWithCondition:(NSinteger)初始化一个。NSConditionLock对象。
- @property(readonly) NSInteger condition 锁的条件。
- -(void)lockWhenCondition:(NSInteger)conditio满足条件时加锁。
- -(BOOL)tryLock尝试加锁。
- -(BOOL)tryLockWhenCondition如果接受对象的condition与给定的condition相等,则尝试获取锁,不足塞线程。
- -(void)unlockWithCondition:(NSInteger)condition解锁,重置锁的条件。
- -(BOOL)lockBeforDate:(NSDate *)limit在指定时间点之前获取锁。
- -(BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit在指定的时间前获取锁。
- @property (nullable ,copy) NSString *name 锁名称。 可以用来控制调用线程顺序
- (void)lg_testConditonLock{
self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:3];
NSLog(@"线程 1");
[self.iConditionLock unlockWithCondition:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[self.iConditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:1];
NSLog(@"线程 3");
[self.iConditionLock unlockWithCondition:0];
});
}
pthread_mutex
- pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型。
- pthread_mutex_lock(pthread_mutex_t mutex);加锁
- pthread_mutex_trylock(*pthread_mutex_t *mutex);加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息
- pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁
- pthread_mutex_destroy(pthread_mutex_t* mutex);使用完锁之后释放锁
- pthread_mutexattr_setpshared();设置互斥锁的范围
- pthread_mutexattr_getpshared();获取互斥锁的范围
- (void)test_pthread_mutex {
//非递归
pthread_mutex_t lock0;
pthread_mutex_init(&lock0, NULL);
pthread_mutex_lock(&lock0);
//要锁的内容
pthread_mutex_unlock(&lock0);
//C语言封装,使用完后要手动释放
pthread_mutex_destroy(&lock0);
//递归
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
//要锁的内容
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
}
dispatch_semaphore_t
- dispatch_semaphore_create(long value)这个函数是创建⼀个dispatch_semaphore_t类型的信号量,并且创建的时候需要指定信号量的⼤⼩。
- dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)等待信号量。如果信号量值为0,那么该函数就会⼀直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值⼤于等于1,该函数会对信号量的值进⾏减1操作,然后返回。
- dispatch_semaphore_signal(dispatch_semaphore_t deem)发送信号量。该函数会对信号量的值进⾏加1操作。
- (void)test_dispatch_semaphore_t {
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3");
});
}
读写锁
GCD实现读写锁,多读单写,读写互斥,dispatch_barrier_async和并行队列实现
//MARK:-读写锁,多读单写,读写互斥
self.queue = dispatch_queue_create("yl", DISPATCH_QUEUE_CONCURRENT);
self.dataDic = [NSMutableDictionary dictionary];
- (void)test_read {
for (NSInteger i = 0; i<10; ++i ) {
dispatch_async(self.queue, ^{
[self readNow:@"name"];
});
}
}
-(NSString *)readNow:(NSString *)key {
__block NSString *readName;
//异步读取
dispatch_sync(self.queue, ^{
readName = self.dataDic[key];
});
return readName;
}
- (void)test_write:(NSDictionary *)dict{
dispatch_barrier_async(self.queue, ^{
[self.dataDic setDictionary:dict];
});
}
@synchronized
可以在多个线程下递归调用
- (void)test_synchronized{
@synchronized (self) {
self.count --;
NSLog(@"%ld",self.count);
}
}
使用clang编译,后可以看出,@synchronized的加锁和解锁是objc_sync_enter和objc_sync_exit两个函数相关,查看objc源码,
objc_sync_enter
objc_sync_exit
- 当传入的参数是空的时候,都不做处理,不会加锁和解锁;
- 当加锁和解锁的时候都和
SyncData相关 - 都调用
ida2Data方法
SyncData的数据结构
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; //单向链表指针,到下一个节点
DisguisedPtr<objc_object> object; //传入参数的封装
int32_t threadCount; // number of THREADS using this block(记录使用block的数量)
recursive_mutex_t mutex;//递归锁
} SyncData;
ida2Data函数
- TLS(Thread Local Storage)就是线程局部存储,是操作系统为线程单独提供的私有空间,能存储只属于当前线程的⼀些数据。
- 从线程的TLS快速缓存中找
- 从线程的中TLS的缓存中找
- 如果从线程缓存中没有找到,从StripedMap表中找,如果找到加的TLS缓存中;
- 如果表中没有找到,加锁,并且放入从StripedMap表中;
总结
@synchronize针对某个对象,也就是我们给@synchronize传的参数,每⼀条线程都有⼀把递归锁,⽽且记录了每条线程加锁的次数,这样就能通过这俩点,对每条线程⽤不同的递归锁来进⾏加锁和解锁的的操作,从⽽达到多线程递归调⽤的⽬的。