多线程的安全问题
多线程涉及到写操作就容易出现问题
对指针本身赋值:
self.userName = @"peak";
访问指针指向的字符串所在的内存区域:
[self.userName rangeOfString:@"peak"]
property分为三类:
pointer Property - > Memory
Primitive Property
内存的理解:
我们只有一个地址总线,一个内存。即使在多线程的情况下,也不可能存在两个线程同时访问同一个块内存区域的场景。
内存的访问是通过一个一个地址总线串行排列访问的,所以在继续后续之前,我们先明确几个结论:
1)内存的访问是串行的,并不会导致多乱或者应用的crash
2)bool,int,long类型是原子性的。
多线程不安全的场景
@property (atomic, assign) int intA;
//thread A
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread A: %d\n", self.intA);
}
//thread B
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread B: %d\n", self.intA);
}
虽然intA声明为原子的,但是结果不一定是20000,因为self.intA = self.intA + 1;不是原子操作,虽然intA的setter和getter方法是原子操作,但是语句不是原子的,这行赋值代码包括读取load + 1(add),赋值(store)三部操作,当线程A store的时候可能线程B已经执行了若干次store了,最后结果小于预期的值。
@property (atomic, strong) NSString* userName;
- (void)setUserName:(NSString *)userName {
if(_uesrName != userName) {
[userName retain];
[_userName release];
_userName = userName;
}
}
不仅仅是赋值操作,还会有retain,release调用。如果property为nonatomic,上述的setter方法就不是原子操作,我们可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash,出现多线程不安全的场景。
场景三
@property (atomic, strong) NSString* stringA;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
//thread B
for (int i = 0; i < 100000; i ++) {
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
虽然stringA是atomic的property,而且在取substring的时候做了length判断,线程B还是很容易crash,因为在前一刻读length的时候self.stringA = @"a very long string";,下一刻取substring的时候线程A已经将self.stringA = @"string";,立即出现out of bounds的Exception,crash,多线程不安全。
@property (atomic, strong) NSArray* arr;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
}
else {
self.arr = @[@"1"];
}
NSLog(@"Thread A: %@\n", self.arr);
}
//thread B
for (int i = 0; i < 100000; i ++) {
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
NSLog(@"Thread B: %@\n", self.arr);
}
同理,即使我们在访问objectAtIndex之前做了count的判断,线程B依旧很容易crash,原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。
总结
atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter方法内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。
atomic会带来一些性能损耗,所以一般用nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。
线程安全的实现方法
非原子性的:
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
加锁:
//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];
加锁以后认为是线程安全的。
加锁方式:
- @synchronized(token)
- NSLock
加锁和关锁要在同一个线程执行,要不会产生不可预知的问题。
递归加锁不要用这个,因为调用这个lock 的方法两次在同一个线程里面会永久的锁住这个线程。
递归用NSRecursiveLock 去实现递归加锁。
- dispatch_semapgore_t
- OSSpinLock
性能损耗由上到下依次减小。