atomic 在 setter 方法里加了鎖
,防止了多線程一直去寫這個 property,造成難以預(yù)計的數(shù)值弟头。
當(dāng)屬性使用 atomic 修飾時,它的讀和寫是原子性的:當(dāng)線程 A 進(jìn)行寫操作群井,這時其他線程的讀或者寫操作會因為該操作而等待。當(dāng) A 線程的寫操作結(jié)束后毫胜,B 線程進(jìn)行寫操作书斜,然后當(dāng) A 線程需要讀操作時,獲得了在 B 線程中修改的值酵使。如果有 C 線程在 A 線程讀操作之前 release 了該屬性荐吉,可能導(dǎo)致程序崩潰。
導(dǎo)致崩潰并不是線程安全問題口渔。所謂線程安全是保證同一時間只有一個線程對該內(nèi)存進(jìn)行訪問
样屠。只要我們使用 getter、setter 方法來訪問,上面的表述中的每一個步驟都只有一條線程在訪問該內(nèi)存痪欲,哪個線程會獲得鎖完全取決于代碼順序混巧,這個崩潰就是程序員自身的問題了。如果繞開 getter勤揩、setter 方法訪問這個屬性,才會造成線程不安全秘蛔,比如使用 KVC陨亡。
一、atomic 是絕對安全的
在 64 位的操作系統(tǒng)下深员,所有類型的指針(包括 void *)都是占用 8 個字節(jié)的负蠕。超過 4 個字節(jié)的基本類型數(shù)據(jù)都會有線程并發(fā)的問題。
那所有的指針類型都會有這個問題倦畅。
以 Objective-C 的 NSArray * 為例子遮糖,如果一個多線程操作這個數(shù)據(jù),會有兩個層級的并發(fā)問題:
- 指針本身
- 指針?biāo)赶虻膬?nèi)存
指針本身也是占用內(nèi)存的叠赐,并且一定是 8
個字節(jié)(64 位系統(tǒng))欲账。第二部分,指針?biāo)赶虻膬?nèi)存芭概,有可能非常大赛不,有可能也就 1 個字節(jié)。
所以考慮 NSArray * array 這個數(shù)據(jù)在進(jìn)行多線程操作的時候罢洲,必須分成兩部分來描述踢故,一個是 &array 這個指針本身,另一個則是它所指向的內(nèi)存 array惹苗。想象現(xiàn)在有兩塊內(nèi)存殿较,一塊是 8 字節(jié),一塊 n 字節(jié)桩蓉,8 字節(jié)里面放的值淋纲,就是 n 字節(jié)內(nèi)存的首地址。
如果用 atomic 修飾之后院究,會有什么影響帚戳?
從內(nèi)存的角度來解釋這個過程。atomic 其實(shí)修飾的是這個指針 &array
儡首,與指針指向的第二部分 n 字節(jié)數(shù)據(jù)沒有任何關(guān)系片任,被 atomic 修飾之后,你不可能隨意去多線程操作這個 8 字節(jié)蔬胯,但是對 8 字節(jié)里面所指向的 n 字節(jié)沒有任何限制对供!
atomic 已經(jīng)完美的履行了它的指責(zé),你不可能對這個 8 字節(jié)進(jìn)行無序的多線程操作,這就夠了呀产场!有問題的是程序員鹅髓,程序員并未對 n 字節(jié)做任何的限制。
二京景、NSMutableArray 本身是線程不安全的
簡單來說窿冯,線程安全就是多個線程訪問同一段代碼,程序不會異常确徙、不 Crash醒串。而編寫線程安全的代碼主要依靠線程同步。
-
不使用 atomic 修飾屬性鄙皇。原因有二:
atomic 的內(nèi)存管理語義是原子性的芜赌,
僅保證
了屬性的 setter 和 getter 方法是原子性的、線程安全的伴逸,但是屬性的其他方法缠沈,如數(shù)組添加/移除元素等并不是原子操作,所以不能保證屬性是線程安全的错蝴。atomic 雖然保證了 getter洲愤、setter 方法線程安全,但是付出的
代價很大
顷锰,執(zhí)行效率要比 nonatomic 慢很多倍(有說法是慢 10-20 倍)禽篱。
總之:使用 nonatomic 修飾 NSMutableArray 對象就可以了,而使用鎖馍惹、dispatch_queue 來保證 NSMutableArray 對象的線程安全躺率。
-
打造線程安全的 NSMutableArray
在《Effective Objective-C 2.0》書中第 41 條:多用派發(fā)隊列,少用同步鎖中指出:使用“串行同步隊列”(serial synchronization queue)万矾,將讀取操作及寫入操作都安排在同一個隊列里悼吱,即可保證數(shù)據(jù)同步。而通過并發(fā)隊列良狈,結(jié)合GCD 的柵欄塊(barrier)來不僅實(shí)現(xiàn)數(shù)據(jù)同步線程安全后添,還比串行同步隊列方式更高效。
GCD 的柵欄塊作用示意圖說明:柵欄塊單獨(dú)執(zhí)行薪丁,不能與其他塊并行遇西。直到當(dāng)前所有并發(fā)塊都執(zhí)行完畢,才會單獨(dú)執(zhí)行這個柵欄塊
線程安全實(shí)現(xiàn)如下:
@interface QSThreadSafeMutableArray() @property (nonatomic, strong) NSMutableArray * MDataArray; @property (nonatomic, strong) dispatch_queue_t MSyncQueue; @end @implementation QSThreadSafeMutableArray - (instancetype)initCommon { if (self = [super init]) { // %p 以 16 進(jìn)制的形式輸出內(nèi)存地址严嗜,附加前綴 0x NSString * uuid = [NSString stringWithFormat:@"com.jzp.array_%p", self]; // 注意:_MSyncQueue 是并行隊列 _MSyncQueue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT); } return self; } - (instancetype)init { if (self = [self initCommon]) { _MDataArray = [NSMutableArray array]; } return self; } - (id)objectAtIndex:(NSUInteger)index { __block id obj; dispatch_sync(_MSyncQueue, ^{ if (index < [_MDataArray count]) { obj = _MDataArray[index]; } }); return obj; } - (NSEnumerator *)objectEnumerator { __block NSEnumerator * enu; dispatch_sync( _MSyncQueue, ^{ enu = [_MDataArray objectEnumerator]; }); return enu; } - (void)insertObject:(id)anObject atIndex:(NSUInteger)index { dispatch_barrier_async( _MSyncQueue, ^{ if (anObject && index < [_MDataArray count]) { [_MDataArray insertObject:anObject atIndex:index]; } }); } - (void)addObject:(id)anObject { dispatch_barrier_async( _MSyncQueue, ^{ if(anObject){ [_MDataArray addObject:anObject]; } }); } - (void)removeObjectAtIndex:(NSUInteger)index { dispatch_barrier_async( _MSyncQueue, ^{ if (index < [_MDataArray count]) { [_MDataArray removeObjectAtIndex:index]; } }); } - (void)removeLastObject { dispatch_barrier_async( _MSyncQueue, ^{ [_MDataArray removeLastObject]; }); } - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject { dispatch_barrier_async( _MSyncQueue, ^{ if (anObject && index < [_MDataArray count]) { [_MDataArray replaceObjectAtIndex:index withObject:anObject]; } }); } - (NSUInteger)indexOfObject:(id)anObject { __block NSUInteger index = NSNotFound; dispatch_sync( _MSyncQueue, ^{ for (int i = 0; i < [_MDataArray count]; i ++) { if ([_MDataArray objectAtIndex:i] == anObject) { index = i; break; } } }); return index; } - (void)dealloc { if (_MSyncQueue) { _MSyncQueue = NULL; } } @end
說明 ①:使用 dispatch queue 實(shí)現(xiàn)線程同步粱檀;將同步與異步派發(fā)結(jié)合起來,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為漫玄,又不會阻塞執(zhí)行異步派發(fā)的線程茄蚯;使用同步隊列及柵欄塊压彭,可以令同步行為更加高效。
說明 ②:NSMutableDictionary 本身也是線程不安全的渗常,實(shí)現(xiàn)線程安全的 NSMutableDictionary 原理同線程安全的NSMutableArray壮不。(代碼見 QSUseCollectionDemo)
-
線程安全的 NSMutableArray 使用
- (void)testQsMutableArray { _MSafeArray = [[QSThreadSafeMutableArray alloc] init]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (NSInteger i = 0; i < 10; i++) { dispatch_async(queue, ^{ NSString * str = [NSString stringWithFormat:@"數(shù)組%d", (int)i+1]; [_MSafeArray addObject:str]; }); } sleep(1); NSEnumerator * enu = [_MSafeArray objectEnumerator]; for (NSObject * object in enu) { NSLog(@"value: %@", object); } }
三、atomic 與 nonatomic 的區(qū)別
在默認(rèn)情況下皱碘,由編譯器生成的屬性的 set询一、get 方法會通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質(zhì)癌椿,則不需要同步鎖健蕊。
盡管沒有指明 atomic 的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì),那它就是"原子的"(atomic))如失,仍然可以在屬性特質(zhì)中寫明這一點(diǎn),編譯器是不會報錯的送粱。
一般 iOS 程序中褪贵,所有屬性都聲明為 nonatomic。這樣做的原因是:
在 iOS 中使用同步鎖的開銷比較大抗俄, 會帶來性能問題脆丁。
一般情況下并不要求屬性必須是"原子的",因為這并不能保證線程安全动雹。若要實(shí)現(xiàn)線程安全的操作槽卫,還需采用更為深層的鎖的機(jī)制。
一個線程在連續(xù)多次讀取某個屬性值的過程中有別的線程在同時改寫該值胰蝠,那么即便將屬性聲明為 atomic 也還是會讀取到不同的屬性值歼培。
因此,iOS 程序一般都會使用 nonatomic 屬性茸塞。但在 Mac OS X 程序時躲庄,使用 atomic 屬性通常都不會有性能瓶頸。
nonatomic 的實(shí)現(xiàn):
- (void)setImage:(UIImage *)image
{
if (_image != image) {
[_image release];
_image = [image retain];
...
}
}
- (UIImage *)image
{
return _image;
}
atomic 的實(shí)現(xiàn):
- (void)setImage:(UIImage *)image
{
@synchronized(self) {
// 鎖
if (_image != image) {
[_image release];
_image = [image retain];
...
}
}
}
- (UIImage *)image
{
@synchronized(self) {
return _image;
}
}
@synchronized 的介紹:
The @synchronized directive is a convenient way to create mutex locks on the fly in Objective-C code. The @synchronized directive does what any other mutex lock would do—it prevents different threads from acquiring the same lock at the same time. In this case, however, you do not have to create the mutex or lock object directly. Instead, you simply use any Objective-C object as a lock token, as shown in the following example:
- (void)myMethod:(id)anObj
{@synchronized(anObj) {
// Everything between the braces is protected by the @synchronized directive.
}
}
The object passed to the @synchronized directive is a unique identifier used to distinguish the protected block. If you execute the preceding method in two different threads, passing a different object for the anObj parameter on each thread, each would take its lock and continue processing without being blocked by the other. If you pass the same object in both cases, however, one of the threads would acquire the lock first and the other would block until the first thread completed the critical section.
As a precautionary measure, the @synchronized block implicitly adds an exception handler to the protected code. This handler automatically releases the mutex in the event that an exception is thrown. This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes.
For more information about the @synchronized directive, see The Objective-C Programming Language.
更準(zhǔn)確的說應(yīng)該是讀寫安全钾虐,但并不是線程安全的噪窘,因為別的線程還能進(jìn)行讀寫之外的其他操作。線程安全需要開發(fā)者自己來保證效扫。