从上图中可以看出,我们只有一个地址总线,一个内存。即使是在多线程的环境下,也不可能存在两个线程同时访问
一块内存区域的场景,内存的访问一定是通过一个地址总线串行排队访问的,所以在继续后续之前,我们先要明确几
个结论:
结论一:内存的访问时串行的,并不会导致内存数据的错乱或者应用的crash。
结论二:如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如
bool,int,long在64位系统下的单次读写都是原子操作。
接下来我们根据上面三种property的分类逐一看下多线程的不安全场景。
值类型Property
先以BOOL值类型为例,当我们有两个线程访问如下property的时候:
@property (nonatomic, assgin) BOOL isDeleted;
//thread 1
bool isDeleted = self.isDeleted;
//thread 2
self.isDeleted = false;
线程1和线程2,一个读(load),一个写(store),对于BOOL isDeleted的访问可能有先后之分,但一定是串行排队的。而
且由于BOOL大小只有1个字节,64位系统的地址总线对于读写指令可以支持8个字节的长度,所以对于BOOL的读和写
操作我们可以认为是原子的,所以当我们声明BOOL类型的property的时候,从原子性的角度看,使用atomic和
nonatomic并没有实际上的区别(当然如果重载了getter方法就另当别论了)。
如果是int类型呢?
@property (nonatomic, assgin) int count;
//thread 1
int curCount = self.count;
//thread 2
self.count = 1;
同理int类型长度为4字节,读和写都可以通过一个指令完成,所以理论上读和写操作都是原子的。从访问内存的角度看
nonatomic和atomic也并没有什么区别。
atomic到底有什么用呢?据我所知,用处有二:
用处一: 生成原子操作的getter和setter。
设置atomic之后,默认生成的getter和setter方法执行是原子的。也就是说,当我们在线程1执行getter方法的时候(创
建调用栈,返回地址,出栈),线程B如果想执行setter方法,必须先等getter方法完成才能执行。举个例子,在32位
系统里,如果通过getter返回64位的double,地址总线宽度为32位,从内存当中读取double的时候无法通过原子操作
完成,如果不通过atomic加锁,有可能会在读取的中途在其他线程发生setter操作,从而出现异常值。如果出现这种异
常值,就发生了多线程不安全。
用处二:设置Memory Barrier
对于Objective C的实现来说,几乎所有的加锁操作最后都会设置memory barrier,atomic本质上是对getter,setter加
了锁,所以也会设置memory barrier。官方文档表述如下:
Note: Most types of locks also incorporate a memory barrier to ensure that
any preceding load and store instructions are completed before entering the critical section.
memory barrier有什么用处呢?
memory barrier能够保证内存操作的顺序,按照我们代码的书写顺序来。听起来有点不可思议,事实是编译器会对我
们的代码做优化,在它认为合理的场景改变我们代码最终翻译成的机器指令顺序。也就是说如下代码:
self.intA = 0; //line 1
self.intB = 1; //line 2
编译器可能在一些场景下先执行line2,再执行line1,因为它认为A和B之间并不存在依赖关系,虽然在代码执行的时
候,在另一个线程intA和intB存在某种依赖,必须要求line1先于line2执行。