多线程的安全问题

多线程涉及到写操作就容易出现问题

对指针本身赋值:

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];

加锁以后认为是线程安全的。

加锁方式:

  1. @synchronized(token)
  2. NSLock

加锁和关锁要在同一个线程执行,要不会产生不可预知的问题。
递归加锁不要用这个,因为调用这个lock 的方法两次在同一个线程里面会永久的锁住这个线程。
递归用NSRecursiveLock 去实现递归加锁。

  1. dispatch_semapgore_t
  2. OSSpinLock

性能损耗由上到下依次减小。

你可能感兴趣的:(多线程的安全问题)