一,线程安全
简单来说就是,在同一个时刻对同一个数据的操作只有一个。而线程不安全,则是在同一个时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。
二,锁的作用
锁作为一种非强制的机制,被用来保证线程安全。每个线程在访问数据时,需要先获取(Acquire)
锁,并在结束的时候释放(release)
锁。如果锁已经被占用,其他获取锁的线程会等待,直到锁从新可用。
三,锁的分类
主要分为两大类:互斥锁
和自旋锁
。条件锁
,递归锁
,信号量
都是上层的分装盒实现。
互斥锁:
互斥锁防止两条线程同时对同一公共资源进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
互斥锁又分为递归锁
和非递归锁
递归锁
:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用
非递归锁
:不可重入,必须等锁释放后才能再次获取锁
互斥锁:@synchronized
,NSLock
, pthread_mutex
, NSConditionLock
, NSCondition
, NSRecursiveLock
自旋锁
线程反复确认锁变量是否可用。由于线程在这一过程保持可执行,因此是一种忙等待。一旦获取到自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
两者的区别
- 自旋锁的优点在于,自旋锁不会引起调用者线程的休眠,所以不会进行线程调度,CPU的时间片轮换的耗时操作。如果在短时间内获得锁,自旋锁的效率高于互斥锁。
- 自旋锁的不足在于,自旋锁一直占用CPU资源 ,在为获得锁的情况下,会一直自旋,相当于死循环。如果不能在短时间内获得锁,会是CPU的效率降低。自旋锁不能实现递归调用。
自旋锁:atomic
、OSSpinLock
、dispatch_semaphore_t
自旋锁
OSSpinLock
自从OSSpinLock出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转
而OSSpinLock忙等的机制就可能造成高优先级一直running等待,占用CPU时间片;而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况.
atomic
atomic原理
自动生成的setter方法胡根据修饰符的不同调用不同的方法,最后统一调用在这里插入代码片reallySetProperty
方法。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
//这是一个条件判断,用于确定操作是否需要进行原子操作(线程安全)
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
- 原子性修饰的属性进行了spinLock加锁处理
- 非原子性的除了没有加锁,其他逻辑与atomic相同
注意:OSSpinLock因为安全问题被废弃了,实际上用os_unfair_lock替代了OSSpinLock。
getter方法也是如此,atomic修饰的属性进行加锁处理
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
atomic只能保证setter和getter方法的线程安全,不能保证数据安全。
- atomic可以保证变量在取值和赋值的时候是线程安全的
- self.index + 1不能保证是安全的,当两个线程同时进行这一操作的时候,在存入,只能起到加1的效果
- self.index = I是能保证setter方法的线程安全的
读写锁
读写锁是一种特殊的自旋锁
,对共享资源的访问化为读者和写着。相对于自旋锁而言,能够提高并发性。它允许同时又多个读者来访问共享资源,最大可能的读者我去诶实际的CPU数。
- 写者是排它性,一个读写锁同时只有一个写者或多个读者,不能同时既有读者又有写者。
- 如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。
// 导入头文件
#import <pthread.h>
// 全局声明读写锁
pthread_rwlock_t lock;
// 初始化读写锁
pthread_rwlock_init(&lock, NULL);
// 读操作-加锁
pthread_rwlock_rdlock(&lock);
// 读操作-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写操作-加锁
pthread_rwlock_wrlock(&lock);
// 写操作-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 释放锁
pthread_rwlock_destroy(&lock);
互斥锁
pthread_mutex
pthread_mutex
就是互斥锁本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠
// 导入头文件
#import <pthread.h>
// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);
@synchronized(互斥递归锁)
@synchronized
可能是日常开发中用的比较多的一种互斥锁,使用比较简单,但并不是在任意场景下都能使用@synchronized
,且它的性能较低。
@synchronized (obj) {}
源码:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {
objc_sync_exit(sync_exit);
}
id sync_exit;
}
_sync_exit(_sync_obj);
}
catch (id e) {_rethrow = e;}
{
struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
}_fin_force_rethow(_rethrow);
}
}
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
@synchronized就是实现了objc_sync_enter
和 objc_sync_exit
两个方法
在objc源码中找到objc_sync_enter
和objc_sync_exit
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
1.如果obj存在,则通过id2data方法获取相应的syncData,对threadCount
、lockCount
进行递增操作。
2.如果obj不存在,则调用objc_sync_nil,通过符号断点得知,这个方法里面是什么都没有做,直接return。
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
1.如果obj存在,则调用id2data
方法获取对应的SyncData
,对threadCount
、lockCount
进行递减操作。
2.如果obj为nil
,什么也不做,直接return
。
- 如果锁的对象
obj
不存在时分别会走objc_sync_nil()
和不做任何操作(源码分析可以先解决简单的逻辑分支)
BREAKPOINT_FUNCTION(
void objc_sync_nil(void)
);
这也是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁。
- 正常情况下,obj存在,会通过id2data方法生成一个
SyncData
对象。
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
nextData
:链表中下一个SyncDataobject:
当前加锁的对象threadCount:
使用该对象进行加锁的线程数mutex:
递归锁
SyncData是通过id2data获得,而且加锁和解锁都是复用该方法
会现在tls中再在cache中查找线程
tls
和cache
表结构分析
- tls哈希表结构中通过
SyncList
结构来组装多线程的情况 SyncData
通过链表的形式组装,记录当前可重入的情况- 下层主要通过tls线程缓存,
cache
缓存来进行处理 - 底层主要有两个东西:
lockCount
,threadCount
,解决递归互斥锁
,解决嵌套重入
总结:
@synchronized
在底层封装的是一把递归锁,所以这个锁是递归互斥锁@synchronized
的可重入,即可嵌套,主要是由于lockCount
和threadCount
的搭配@synchronized
使用链表的原因是链表方便下一个data
的插入- 不能使用非OC对象作为加锁条件,
id2data
中接收参数为id类型 - 多次锁同一个对象会有什么后果吗,会从高速缓存中拿到data,所以只会锁一次对象
- @
synchronized
性能低,因为在底层增删改查消耗了大量性能 - 加锁对象不能为
nil
,否则加锁无效,不能保证线程安全
NSLock
NSLock
是对互斥锁 pthread_mutex
的简单封装。
- (void)test {
self.testArray = [NSMutableArray array];
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
self.testArray = [NSMutableArray array];
[lock unlock];
});
}
}
NSLock非递归锁在递归调用时,会造成堵塞,并非死锁。第一次加锁之后还没有出锁就进行递归调用,第二次加锁就堵塞了线程(因为不能查询缓存)
- (void)test {
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
NSLog(@"加锁前");
[lock lock];
NSLog(@"加锁后");
if (value > 0) {
NSLog(@"value——%d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
打印结果:
2024-07-31 11:17:59.586099+0800 死锁[94888:3635993] 加锁前
2024-07-31 11:17:59.586152+0800 死锁[94888:3635993] 加锁后
2024-07-31 11:17:59.586190+0800 死锁[94888:3635993] value --- 10
2024-07-31 11:17:59.586243+0800 死锁[94888:3635993] 加锁前
解决方案:
- 移动锁的位置
NSLock* lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
[lock lock];
block = ^(int value) {
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
};
block(10);
[lock unlock];
});
输出结果为依次从10打印到1
- 使用
@synchronized
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
@synchronized (self) {
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
}
};
block(10);
});
输出结果为依次从10打印到1
- 使用递归锁
NSRecursiveLock
替换NSLock
NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
//for(int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
//}
只输出一边从10到1。
注意这里加入循环会造成崩溃,下面会讲
总结:
- 在同一线程调用NSLock的两次lock方法将永久锁定线程
- 向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。
NSRecursiveLcok
使用:
NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
NSLog(@"加锁前");
[lock lock];
NSLog(@"加锁后");
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
递归锁会出现死锁 ------ 前后代码相互等待便会产生死锁。
在上述代码加上for循环。就会崩溃
NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
for(int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
[lock lock];
NSLog(@"加锁后");
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
[lock unlock];
};
block(10);
});
}
原因: for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁——找不到解锁的出口
即 线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁
解决: 可以采用使用缓存的@synchronized,因为它对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性
for(int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^block)(int);
block = ^(int value) {
@synchronized (self) {
if(value > 0) {
NSLog(@"value --- %d", value);
block(value - 1);
}
}
};
block(10);
});
}
NSCondition
NSCondition
是一个条件锁,可能平时用的不多,但与信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足。
NSCondition的对象实际上作为一个锁和一个线程检查器。
1.锁主要为了当检测条件时保护数据源,执行条件引发的任务
2.线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞
用法:
//初始化
NSCondition *condition = [[NSCondition alloc] init]
//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition lock];
//与lock 同时使用
[condition unlock];
//让当前线程处于等待状态
[condition wait];
//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];
1.NSCondition
是对mutex和cond的一种封装(cond就是用于访问和操作特定类型数据的指针)
2.wait
操作会阻塞线程,使其进入休眠状态,直至超时
3.signal
操作是唤醒一个正在休眠等待的线程
4.broadcast
会唤醒所有正在等待的线程
NSConditionLock
NSConditionLock定义:条件锁,一旦一个线程获得锁,其他线程一定等待。其本质就是NSCondition + Lock。
相比NSConditionLock而言,NSCondition使用比较繁琐,所以推荐使用NSConditionLock,
//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
//表示conditionLock期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁
[conditionLock lock];
//表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
[conditionLock lockWhenCondition:A条件];
//表示释放锁,同时把内部的condition设置为A条件
[conditionLock unlockWithCondition:A条件];
// 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理
return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间];
NSConditionLock
是NSCondition
加线程的封装
NSConditionLock
可以设置锁条件,而NSCondition
只是通知信号
总结:
OSSpinLock
不再安全,底层用os_unfair_lock
代替atomic
只能保证setter
和getter
时线程安全,所以更多使用nonatomic来修饰- 读写锁更多用栅栏函数来实现
@synchronized
底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁- NSLock,NSRecursiveLock,NSCcondition和NSCconditionLock底层都是对
pthread_mutex
的封装 NSCondition
和NSCondiyionLock
是条件锁,当满足了某一个条件时才能进行操作和型号量dispatch_semaphore
类似- 普通场景下涉及到线程安全,可以用
NSLock
- 循环调用时用
NSRecursiveLock
- 循环调用且有线程影响时,请注意死锁,如果有死锁问题就使用
@synchronized