iOS进阶之atomic一定是线程安全的吗(10)

IOS项目中nonatomic和atomic分析

//有两个属性,分别设置为nonatomic和atomic
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic, strong) NSString *name;
@property (atomic, assign) int number;

@end

一、 10000个异步任务,修改name属性的值

- (void)nonatomic{
    for (NSInteger i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.name = [NSString stringWithFormat:@"name:%ld", i];
        });
    }
}

执行结果:崩溃,崩溃原因是在子线程Thread8上,对象释放了。
iOS进阶之atomic一定是线程安全的吗(10)
结果分析:
1、在MRC模式下,属性name的set方法如下

-(void)setName:(NSString *)name{
    if (_name != name) {
        [_name release];
        [name retain];
        _name = name;
    }
}

2、虽然在ARC模式下不用写其set方法,但是在底层还是会走到这里
3、因为是多线程,且没有加锁保护,所以在一个线程走到[_name release]后,可能在另一个线程又一次去释放,这时候造成崩溃。
4、把name属性的nonatomic改成atomic就不会崩溃了,因为atomic加锁了,是安全的。

二、接着上步说用atomic就安全了,再进一步分析

number属性使用atomic修饰的

- (void)atomic{
        self.number = 0;
        dispatch_apply(10000, dispatch_get_global_queue(0, 0), ^(size_t index) {
            self.number ++;
        });
        NSLog(@"_number:%d", self.number);
    }

执行结果:执行结果并不是10000,而且每次运行结果都不一样,即运行结果不可预见。
iOS进阶之atomic一定是线程安全的吗(10)
结果分析:

_number++等价于
 int temp = _number+1;
 _number = temp;

虽然atomic保证了number属性线程安全了,但是并不能保证temp变量的线程安全,又因为是多线程的,所以有可能同时执行多次 int temp = _number+1;才执行一次 _number = temp;导致结果每次都不同,而且结果不可预知。

atomic VS nonatomic

  • atomic是原子性的,nonatomic是非原子性的。
  • atomic原子性并不能保证多线程安全,只是能保证数据的完整性
  • 这个完整性体现在:使用者总能取到完整的值

但是如果一个线程在多次修改某个属性时,另一个线程去读取属性时,可能会取到未修改好的属性,下面我们将举例来证明:

源码:

@interface ViewController () 
@property (nonatomic,strong) NSArray *dataArray;
@end

@implementation ViewController
  
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.dataArray = @[@"1",@"2",@"3",@"4",@"5"];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<100; i++) {
            self.dataArray = @[@"1",@"2"];
        }
        NSLog(@"--1111111=--%@",self.dataArray);
    });
    
    for (int i=0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"--222222=--%@",self.dataArray);
        });
    }
}

iOS进阶之atomic一定是线程安全的吗(10)
使用nonatomic修饰dataArray,会崩溃,但是并不是每次都崩,差不多10次左右就会崩一次。
我们用nonatomic,strong修饰的数组dataArray,在一个异步队列的任务中持续地去修改这个属性,在另一个异步队列的任务中持续地去读取这个属性,由于dataArray是由strong来修饰的,那么在dataArray的setter方法中,其实是先保留新值,后释放旧值,再将指针指向新值,所以在后面这个异步队列的任务中,取到的dataArray可能是一个已经被释放的僵尸对象,所以会崩溃。

当线程A进行写操作,这时其他线程的读或者写操作会因为该操作而等待。当A线程的写操作结束后,B线程进行写操作,然后当A线程需要读操作时,却获得了在B线程中的值,这就破坏了线程安全,如果有线程C在A线程读操作前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。
iOS进阶之atomic一定是线程安全的吗(10)
使用atomic修饰dataArray,不会崩溃在这段代码中。

那么还有一个问题:atomic是线程安全的吗?

线程安全:多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的。

下面让我们来做个小实验
iOS进阶之atomic一定是线程安全的吗(10)
对用atomic修饰的数组连续修改时,读取属性,输出结果为:
iOS进阶之atomic一定是线程安全的吗(10)
我们对在一个异步队列的任务中,对dataArray多次写入,并在另一个异步队列中多次读取dataArray,从输出结果中发现,得到的结果并不一样,所以使用atomic也并不能保证线程安全。

atomic 原理和作用

设置成员变量的@property属性时,默认为atomic,提供多线程安全。
在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,setter函数会变成下面这样:

{lock}
    if (property != newValue) { 
        [property release]; 
         property = [newValue retain]; 
    }
{unlock}

Atomic不能保证对象多线程的安全,它只是能保证你访问的时候给你返回一个完好无损的Value而已。atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,比如上面的例子。

也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。

atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。仅仅使用atomic并不会使得对象线程安全,我们还要为对象线程添加lock来确保线程的安全。

nonatomic对象setter和getter方法的实现:
- (void)setCurrentImage:(UIImage *)currentImage
{
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];
    }
}
- (UIImage *)currentImage
{
    return _currentImage;
}
atomic对象setter和getter方法的实现:
- (void)setCurrentImage:(UIImage *)currentImage
{
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
        }
    }
}

- (UIImage *)currentImage
{
    @synchronized(self) {
        return _currentImage;
    }
}

各抒己见

简而言之就是只保证setter和getter的操作完整性,不保证属性的线程安全,atomic修饰后,还是可以多线程同时修改这个值的,至于这个值最终是什么,天知道。
atomic修饰后, 不会出现多线程同时修改这个值的。至于这个值最终是什么,无法确定,是因为你不知道多线程的调用顺序,也就无法判断最终的值是什么。

  也就是要注意:atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。如下列所示:比如:@property(atomic,strong)NSMutableArray *arr;  

如果一个线程循环的读数据,一个线程循环写数据,那么肯定会产生内存问题,因为这和setter、getter没有关系。如使用[self.arr objectAtIndex:index]就不是线程安全的。好的解决方案就是加锁。

atomic在set方法里加了锁,防止了多线程一直去写这个property,造成难以预计的数值。但这也只是读写的锁定。跟线程安全其实还是差一些。看下面。

@interface MONPerson : NSObject 
@property (copy) NSString * firstName; 
@property (copy) NSString * lastName; 
- (NSString *)fullName; 
@end

Thread A:
p.firstName = @"Rob";
Thread B:
p.firstName = @"Robert";
Thread A:
label.string = p.firstName; // << uh, oh -- will be Robert

但是如果有个C也在写,D在读取,D会读到一些随机的值(ABC修改的值),这就不是线程安全的了。最好的方法是使用lock。

Thread A:
[p lock]; // << wait for it… … … …
// Thread B now cannot access 
pp.firstName = @"Rob";
NSString fullName = p.fullName;
[p unlock];
// Thread B can now access plabel.string = fullName;

Thread B:
[p lock]; // << wait for it… … … …
// Thread A now cannot access p…
[p unlock];

结论:用atomic修饰后,这个属性的setter、getter方法是线程安全的,但是对于整个对象来说不一定是线程安全的。

参考

IOS项目中nonatomic和atomic分析
atomic是线程安全的吗?
iOS atomic 对象是线程不安全的原因以及与 nonatomic 的区别

上一篇:20190905 Lombok常用注解


下一篇:Hystrix 隔离策略细粒度控制_一点课堂(多岸学院)