atomic作用:多線程下將屬性設(shè)置為atomic可以保證讀取數(shù)據(jù)的一致性。因?yàn)樗麑⒈WC數(shù)據(jù)只能被一個線程占用,也就是說一個線程對屬性進(jìn)行寫操作時,會使用自旋鎖鎖住該屬性。不允許其他的線程對其進(jìn)行讀取操作了。
但是它有一個很大的缺點(diǎn):因?yàn)樗褂米孕i鎖住該屬性,因此它會消耗更多的資源,性能會很低。要比nonatomic慢20倍。
內(nèi)部實(shí)現(xiàn):property 的 atomic 是采用 spinlock_t (自旋鎖)實(shí)現(xiàn)的。
// getter方法
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic)
{
// ...
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// ...
}
//setter方法
// setter
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// ...
if (!atomic)
{
oldValue = *slot;
*slot = newValue;
}
else
{
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// ...
}
小結(jié)
簡而言之,atomic的作用只是給getter和setter加了個鎖,atomic只能保證代碼進(jìn)入getter或者setter函數(shù)內(nèi)部時是安全的,一旦出了getter和setter,多線程安全只能靠程序員自己保障了。所以atomic屬性和使用property的多線程安全并沒什么直接的聯(lián)系。另外,atomic由于加鎖也會帶來一些性能損耗,所以我們在編寫iOS代碼的時候,一般聲明property為nonatomic,在需要做多線程安全的場景,自己去額外加鎖做同步。
指針Property指向的內(nèi)存區(qū)域
property可以分為值類型和引用類型。值類型就是一些基本的數(shù)據(jù)類型。而引用類型就是指的各種對象。聲明為指針,指向這個屬性所在的內(nèi)存區(qū)域。
因此當(dāng)我們訪問一個屬性的時候,有可能訪問的是指針本身,還有可能訪問的是這個指針指向的內(nèi)存區(qū)域。
如:
self.userName = @"nick";
這里訪問的就是指針本身,對指針進(jìn)行賦值。
[self.userName rangeOfString:@"peak"];
是在訪問指針指向的字符串所在的內(nèi)存區(qū)域。
指針本身也是占用內(nèi)存的,是固定的8個字節(jié)。
而指針指向的內(nèi)存區(qū)域占用的內(nèi)存就無法固定了,有可能很大,也有可能很小。我們暫且定為n。
下面舉例說明一下:
@property (atomic) NSArray *array;
atomic修飾的實(shí)際上是這個指針,也就是占8個字節(jié)內(nèi)存的指針,因此就不可能隨意使用多線程來操作這塊內(nèi)存的。因?yàn)檫@塊內(nèi)存是原子性的。是線程安全的。
真正不安全的是指針指向的那塊內(nèi)存區(qū)域,他是非原子性的,當(dāng)多個線程去操作這塊內(nèi)存的時候,就會出現(xiàn)不安全的情況。
下面我們再看兩個例子:
@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);
}
因?yàn)閍tomic修飾的是指針區(qū)域,而不是內(nèi)存區(qū)域,但是我們通過self.stringA.length 和substringWithRange訪問的是property的內(nèi)存區(qū)域,在前一刻讀length的時候self.stringA = @"a very long string";,下一刻取substring的時候線程A已經(jīng)將self.stringA = @"string";,立即出現(xiàn)out of bounds的Exception,crash。主要原因就是self.stringA.length >= 10和NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];代碼之間內(nèi)存區(qū)域被其他線程修改了。
同樣的場景還存在對集合類操作的時候,比如:
@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所指向的內(nèi)存區(qū)域被其他線程修改了。
所以真正要操心的是這一類內(nèi)存區(qū)域的訪問,即使用atomatic聲明屬性也沒用。
如何做到多線程安全?(加鎖)
為了避免這種crash可以通過一下代碼:
//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];
和
//thread A
[_lock lock];
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);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
[_lock unlock];