关于atomic为什么说不安全?
个人言:
现在网上一搜索都是说atomic是不安全的,这是一个坑人的说法,这样很容易把别人带沟里去的,其实atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,每次只能一个线程去调用setter方法或者getter。这句话什么意思呢????
比如说如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性,这就是atomic起作用了。但是,你不能同时掉settter方法,却可调getter方法啊,如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值,所以atomic只是不能保证对象的线程安全,而不能说它是不安全的。网上说法说是,atomic要比nonatomic慢大约20倍,这个还真不知道以什么标准去算这个20倍,知道就好了。
下面是原文转载:《关于atomic为何不安全? 》解释的很详细,因为是论坛里面看起来比较繁琐,所以就整理记录一下,学习了。
首先我表明自己的立场先,实际上atomic 是安全的,而且是绝对安全的 。
atomic 实际上就是原子操作,这个概念其实并不新鲜,早在linux系统下编程本身也是有这个东西的,所谓原子,就是不可再化分,已经是最小的操作单位(所谓操作指的是对内存的读写)网上很多地方都在讨论oc下的atomic 不安全,不能保证数据的并发性,实际上有一点误导了大家,认为atomic 本身是不安全的,实际上,并非atomic 不安全,而是网上一些说法有问题 。
atomic 确实是线程安全的,但是我发现好多人都被网络上的一些说法误导了,下面就来剖析一下 atomic 是如何在有效范围内安全的 。
所谓一个数据的线程安全,简单点来说就是这块数据即使有多个线程同时读写,也不会出现数据的错乱,内存的最后状态总是可以预见的,如果这块内存的数据被一个多线程读写之后,出现的结果是不可预见的,那么就可以说这块内存是“线程不安全的”。其实这个状态很容易理解,同一个箱子,有的人在里面放球,有的人从里面拿,如果没有一个有规则的顺序,都乱哄哄的一起进行,那么最后箱子里几个球只能靠猜了,所以atomic 就是解决“线程操作同一块内存顺序”的一个方案 。
atomic 实际上相当于一个引用计数器,这个大家很熟悉,如果被标记了atomic,那么被标记了的内存本身就有了一个引用计数器,第一个占用这块内存的线程,会给这个计数器+1,在这个线程操作这块内存期间,其他线程在访问这个内存的时候,如果发现“引用计数器”不为0,则阻塞,实际上阻塞并不等于休眠,他是基于cpu轮询片,休眠除非被叫醒,否则无法继续执行,阻塞则不同,每个cpu 轮询片到这个线程的时候都会尝试继续往下执行,可见 阻塞相对于休眠来讲,阻塞是主动的,休眠是被动的,如果引用计数器为0,轮询片到来,则先给这块内存的引用计数器+1,然后再去操作,atomic 实现操作序列化的具体过程大概就是如此,说来很容易理解,但是为什么还会有歧义?
首先我们从最基本的数据类型说起,char 、int、 long、dobule 比如是这四种,如果在64位系统下,他们分别占1、4、8、8 个字节。想象一下,1个字节也是内存,8个字节也是内存,只要是内存,就有可能产生所谓的资源竞争,也就是多线程并发的问题。据我所知,char 、int 是绝对线程安全的,也就是说 系统对 char 、int 类型数据的操作,要么不操作,要么绝对会把这些字节全部操作完,不会并发的问题,也就不会产生所谓所线程访问的问题。也就说,即使是一个并发的多线程,对一个char 或者int 字节进行操作,无论怎么操作,无论情况如何极端,都不会导致数据的错乱,可以这么说:char 、int 是绝对线程安全的。
再来说 long、double 这两个并没有char 、int 那般的特殊待遇,实际上,这两个分别占了8个字节,对一个long型数据的读写,操作系统有可能分两部分进行,一部分是高位,一部分是低位,所以两个线程同时操作long数据,有可能导致数据不同步,但是据我写的demo测试,这种情况很难出现,因为一个long型的数据最多也就8个字节,对着8个字节都是微妙甚至纳秒级别,一个cpu的轮询片的周期,极大情况下都会比这个时间长,也就说,即使long 不是线程安全的,但是由于本身字节非常少,读写速度极快,快到比cpu轮询的时间片都块的情况下,实际上即使不线程安全,两个线程也不会同时读写这块内存。但凡事都有例外,demo 不能出现不代表他就是线程安全的,只能说绝大部分情况下,多线程操作一个long 数据问题是不大的。
继续说double,double 类型的数据同样占8个字节,按道理来说,他读写的速度应当和long 一样快。实际上,也是这样的。但是。一个线程读写内存仅仅只是一个方面,cpu 需要对数据进行计算,这个计算的中间结果一般都会放到寄存器或者cpu 高速缓存,double 数据计算的复杂度远非long型所能比,因此double 数据类型相比long 型数据更容易出现并发的问题,但是我实际测试的结果,也没有出现什么问题。
说到这里其实应该总结一下窍门了,什么样的数据会存在多线程的问题?什么样的数据不会呢?
可以想象一下,如果一个数据占的内存特别大,读写这块数据需要的时间也就越长,如果这个时间长度远远超过线程调度的轮询片,那么就有极大可能出现并发问题。单单这样说其实还是不能归纳那些数据才会出现并发问题,你就记住,小于等于4个字节的基本类型数据,比如char、 short、 int 等等等等,都是线程安全的,只要大于这个规定,都不是线程安全的。
ok 我们继续讨论一种特殊的数据类型,指针类型。
我们知道,在64位的操作系统下,所有类型的指针,包括void * 都是占用8个字节的。我们上面已经说了,超过4个字节的基本类型数据都会有线程并发的问题。那所有的指针类型都会有这个问题。以oc 下的 NSArray * 为例子,如果一个多线程操作这个数据,会有两个层级的并发问题:
1、指针本身
2、指针所指向的内存
上面已经说了,指针本身也是占用内存的,并且一定是8个字节,第二部分,指针所指向的内存,这个占多少字节就不一定了,有可能非常大,有可能也就1个字节
所以我们考虑NSArray * array 这个数据array 多线程操作的时候,必须分成两部分来描述,一个是&array这个指针本身,另一个则是它所指向的内存 array,大家注意下 &array 和 array 的区别 ,其实不用纠结,你就想象现在有两块内存,一块是8字节,一块n字节,8字节里面放的值,就是n字节内存的首地址,
ok 现在联系上atomic,如果用@property(atomic)NSArray *array 修饰之后,会有什么影响?
首先第一点,你要记住,@property(atomic)NSArray *array 其实修饰的是这个指针,也就是这个8字节内存,跟第二部分数据n字节没有任何关系,被atomic 修饰之后,你不可能随意去多线程操作这个8字节,但是对8字节里面所指向的n字节没有任何限制!这就是所有网络上所说的 atomic 不安全的真想 !
我们来看一下,这能怪atomic?
本身你修饰的是一个指针,并且atomic 已经完美的履行了它的指责,你现在不可能对这个8字节进行无序的多线程操作,这就够了呀!atomic没有任何鸟问题。有问题的是人,你本身并未对n字节做任何的限制,所以把问题怪罪到atomic 上真的是很不合理。
另外我们回忆一下网络的说法,说atomic 只对 get 和 set 方法起作用,这个说法很容易理解,我们知道,这个8字节里面存储的数据,是n字节数据的头地址,如果更改8字节数据的内容,那么最后通过这个指针访问到的数据就会完全不一样,8字节相当于楼管,里面的数据相当于整栋楼的钥匙,给你不同的钥匙,你是不是就进的是不同的房间?
通过atomic 我们可以保证这个“指针”被有序的访问,也仅仅只能保证到这。现在我们有一个8字节的指针,假如我们做一个初始化 NSArray *array = [[NSArray alloc] init] 这个操作。实际上这个操作有两个意思:
1、给8字节赋值
2、开辟了一块n字节的内存区
考虑场景一:
我们只说这8字节的地址复制,如果没有atomic 修饰,并且假设现在有两个线程正在操作这个指针,一个就是上面的初始化线程,另一个线程就是读这个8自己的指针。
首先,假如8字节内部存放的是0x1122334455667788 ,8字节需要写入这个值,但与此同时,很不巧,另一个读线程现在要读这个8字节里面的值,假如这个8字节只写了一半的时候 ,另一个线程来读,那它读到的可能是 0x1122334400000000 , 实际上,等他读完之后,写线程仍然还未完成。 这时候,[[NSArray alloc] init] 的头地址正确的应该是0x1122334455667788 ,而读线程读到的是0x1122334400000000 这时候会出现什么情况?
最好的情况,无非就是个野指针,因为谁也不知道这块地址是否有效或者是否有什么重要的数据,野指针会导致啥不多说了,最坏的情况,这个野地址指向的是重要的一段数据。。。后果可想而知。
所以 atomic 的意义就在于此,在0x1122334455667788 写完之前,读线程是无法读取的,同样的道理,在读线程正在读的过程中,写线程是无法改变8字节的 ,atomic 能避免这8字节的值因为多线程的原因被意外破坏,仅此而已。
考虑场景二:
假如现在有atomic 修饰,假如现在有两个线程正在操作这个指针,根据上面的结论,他俩“先后”正确的获取到了内存地址,也就说,他俩都先后、正确的找到了8字节内容所指向的n字节内容,虽然找到这n个字节内容的顺序有先后,但是不影响这两个线程同时去操作这n个字节的数据,这样问题又来了,两个线程同时去操作n字节内容,如果两个线程都是读线程,一般不会有问题,但是假如至少有一个是写线程,那问题又来了,还是一个读写同步的问题,因此 atomic 虽然规范了 找到这n字节内容的先后顺序,但是它不能规范对着n个字节内容的读写。这就是atomic 的局限性。
如果是指针变量,需要加锁,如果是基本变量,不用考虑,不需要加锁
假如现在只有一个cpu 核心,但是我现在想线程并发,怎么办? 假如有1、2 两个线程想并发
cpu 就会执行1下1 然后再执行1下2 然后再执行1下1.。。。。。。。。。。 如此不停的切换,这就是cpu 轮询