多線程涉及到寫(xiě)操作就容易出現(xiàn)問(wèn)題
對(duì)指針本身賦值:
self.userName = @"peak";
訪問(wèn)指針指向的字符串所在的內(nèi)存區(qū)域:
[self.userName rangeOfString:@"peak"]
property分為三類:
pointer Property - > Memory
Primitive Property
內(nèi)存的理解:
我們只有一個(gè)地址總線,一個(gè)內(nèi)存拂蝎。即使在多線程的情況下穴墅,也不可能存在兩個(gè)線程同時(shí)訪問(wèn)同一個(gè)塊內(nèi)存區(qū)域的場(chǎng)景。
內(nèi)存的訪問(wèn)是通過(guò)一個(gè)一個(gè)地址總線串行排列訪問(wèn)的温自,所以在繼續(xù)后續(xù)之前玄货,我們先明確幾個(gè)結(jié)論:
1)內(nèi)存的訪問(wèn)是串行的,并不會(huì)導(dǎo)致多亂或者應(yīng)用的crash
2)bool悼泌,int松捉,long類型是原子性的。
多線程不安全的場(chǎng)景
@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聲明為原子的馆里,但是結(jié)果不一定是20000隘世,因?yàn)閟elf.intA = self.intA + 1;不是原子操作,雖然intA的setter和getter方法是原子操作鸠踪,但是語(yǔ)句不是原子的丙者,這行賦值代碼包括讀取load + 1(add),賦值(store)三部操作,當(dāng)線程A store的時(shí)候可能線程B已經(jīng)執(zhí)行了若干次store了,最后結(jié)果小于預(yù)期的值营密。
@property (atomic, strong) NSString* userName;
- (void)setUserName:(NSString *)userName {
if(_uesrName != userName) {
[userName retain];
[_userName release];
_userName = userName;
}
}
不僅僅是賦值操作械媒,還會(huì)有retain,release調(diào)用卵贱。如果property為nonatomic滥沫,上述的setter方法就不是原子操作,我們可以假設(shè)一種場(chǎng)景键俱,線程1先通過(guò)getter獲取當(dāng)前_userName兰绣,之后線程2通過(guò)setter調(diào)用[_userName release];,線程1所持有的_userName就變成無(wú)效的地址空間了编振,如果再給這個(gè)地址空間發(fā)消息就會(huì)導(dǎo)致crash缀辩,出現(xiàn)多線程不安全的場(chǎng)景臭埋。
場(chǎng)景三
@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的時(shí)候做了length判斷,線程B還是很容易crash健无,因?yàn)樵谇耙豢套xlength的時(shí)候self.stringA = @"a very long string";荣恐,下一刻取substring的時(shí)候線程A已經(jīng)將self.stringA = @"string";叠穆,立即出現(xiàn)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);
}
同理渗磅,即使我們?cè)谠L問(wèn)objectAtIndex之前做了count的判斷,線程B依舊很容易crash始鱼,原因也是由于前后兩行代碼之間arr所指向的內(nèi)存區(qū)域被其他線程修改了。
總結(jié)
atomic的作用只是給getter和setter加了個(gè)鎖医清,atomic只能保證代碼進(jìn)入getter或者setter方法內(nèi)部時(shí)是安全的,一旦出了getter和setter状勤,多線程安全只能靠程序員自己保障了鞋怀。所以atomic屬性和使用property的多線程安全并沒(méi)什么直接的聯(lián)系持搜。
atomic會(huì)帶來(lái)一些性能損耗,所以一般用nonatomic葫盼,在需要做多線程安全的場(chǎng)景,自己去額外加鎖做同步贫导。
線程安全的實(shí)現(xiàn)方法
非原子性的:
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];
加鎖以后認(rèn)為是線程安全的。
加鎖方式:
- @synchronized(token)
- NSLock
加鎖和關(guān)鎖要在同一個(gè)線程執(zhí)行孩灯,要不會(huì)產(chǎn)生不可預(yù)知的問(wèn)題。
遞歸加鎖不要用這個(gè)峰档,因?yàn)檎{(diào)用這個(gè)lock 的方法兩次在同一個(gè)線程里面會(huì)永久的鎖住這個(gè)線程寨昙。
遞歸用NSRecursiveLock 去實(shí)現(xiàn)遞歸加鎖。
- dispatch_semapgore_t
- OSSpinLock
性能損耗由上到下依次減小舔哪。