前言
一塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源,比如多個線程訪問同一個對象、同一個變量、同一個文件和同一個方法等。因此當(dāng)多個線程訪問同一塊資源時,很容易會發(fā)生數(shù)據(jù)錯誤及數(shù)據(jù)不安全等問題。因此要避免這些問題,我們需要使用“線程鎖”來實現(xiàn)。
下面從以下幾個方面來談?wù)刬OS創(chuàng)建鎖的方法:
1、使用關(guān)鍵字
1-1、@synchronized(互斥鎖)
優(yōu)點:使用@synchronized關(guān)鍵字可以很方便地創(chuàng)建鎖對象,而且不用顯式的創(chuàng)建鎖對象。
缺點:會隱式添加一個異常處理來保護(hù)代碼,該異常處理會在異常拋出的時候自動釋放互斥鎖。而這種隱式的異常處理會帶來系統(tǒng)的額外開銷,為優(yōu)化資源,你可以使用鎖對象。
2、GCD-信號量(“互斥鎖”)
2-1、dispatch_semaphore
3、 “Object-C”語言
3-1、NSLock(互斥鎖)
3-2、NSRecursiveLock(遞歸鎖)
條件鎖,遞歸或循環(huán)方法時使用此方法實現(xiàn)鎖,可避免死鎖等問題。
3-3、NSConditionLock(條件鎖)
使用此方法可以指定,只有滿足條件的時候才可以解鎖。
3-4、NSCondition
3-5、NSDistributedLock(分布式鎖)
在IOS中不需要用到,也沒有這個方法,因此本文不作介紹,這里寫出來只是想讓大家知道有這個鎖存在。
如果想要學(xué)習(xí)NSDistributedLock的話,你可以創(chuàng)建MAC OS的項目自己演練,方法請自行Google,謝謝。
4、C語言
4-1、pthread_mutex(互斥鎖)
4-2、pthread_mutex(recursive)(遞歸鎖)
4-3、OSSpinLock
4-4、POSIX(條件鎖)
1、使用關(guān)鍵字
1-1、@synchronized
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要線程同步的操作1 開始");
sleep(3);
NSLog(@"需要線程同步的操作1 結(jié)束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要線程同步的操作2");
}
});
執(zhí)行結(jié)果:
2017-11-22 14:18:19.814511+0800 test[14626:621364] 需要線程同步的操作1 開始
2017-11-22 14:18:22.818111+0800 test[14626:621364] 需要線程同步的操作1 結(jié)束
2017-11-22 14:18:22.818334+0800 test[14626:621372] 需要線程同步的操作2
@synchronized(obj)指令使用的obj為該鎖的唯一標(biāo)識,只有當(dāng)標(biāo)識相同時,才為滿足互斥,如果線程2中的@synchronized(obj)改為@synchronized(self),剛線程2就不會被阻塞,@synchronized指令實現(xiàn)鎖的優(yōu)點就是我們不需要在代碼中顯式的創(chuàng)建鎖對象,便可以實現(xiàn)鎖的機(jī)制,但作為一種預(yù)防措施,@synchronized塊會隱式的添加一個異常處理例程來保護(hù)代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。所以如果不想讓隱式的異常處理例程帶來額外的開銷,你可以考慮使用鎖對象。
2、GCD-信號量(“互斥鎖”)
2-1、dispatch_semaphore
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要線程同步的操作1 開始");
sleep(2);
NSLog(@"需要線程同步的操作1 結(jié)束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要線程同步的操作2");
dispatch_semaphore_signal(signal);
});
輸出結(jié)果:
2017-11-22 18:03:36.546146+0800 test[22770:1324874] 需要線程同步的操作1 開始
2017-11-22 18:03:38.551063+0800 test[22770:1324874] 需要線程同步的操作1 結(jié)束
2017-11-22 18:03:38.551440+0800 test[22770:1324870] 需要線程同步的操作2
如果把超時時間設(shè)置為<2s的時候,執(zhí)行的結(jié)果就是:
輸出結(jié)果:
2017-11-22 18:08:07.241971+0800 test[23008:1348365] 需要線程同步的操作1 開始
2017-11-22 18:08:08.246878+0800 test[23008:1348366] 需要線程同步的操作2
2017-11-22 18:08:09.245724+0800 test[23008:1348365] 需要線程同步的操作1 結(jié)束
dispatch_semaphore是GCD用來同步的一種方式,與他相關(guān)的共有三個函數(shù),分別是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
(1)dispatch_semaphore_create的聲明為:
dispatch_semaphore_t dispatch_semaphore_create(long value);
傳入的參數(shù)為long,輸出一個dispatch_semaphore_t類型且值為value的信號量。
值得注意的是,這里的傳入的參數(shù)value必須大于或等于0,否則dispatch_semaphore_create會返回NULL。
(2)dispatch_semaphore_signal的聲明為:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
這個函數(shù)會使傳入的信號量dsema的值加1;
(3) dispatch_semaphore_wait的聲明為:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
這個函數(shù)會使傳入的信號量dsema的值減1;這個函數(shù)的作用是這樣的,如果dsema信號量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語句,并且將信號量的值減1;如果desema的值為0,那么這個函數(shù)就阻塞當(dāng)前線程等待timeout(注意timeout的類型為dispatch_time_t,不能直接傳入整形或float型數(shù)),如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號量,那么就繼續(xù)向下執(zhí)行并將信號量減1。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時,其所處線程自動執(zhí)行其后語句。
dispatch_semaphore 是信號量,但當(dāng)信號總量設(shè)為 1 時也可以當(dāng)作鎖來。在沒有等待情況出現(xiàn)時,它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時,性能就會下降許多。相對于 OSSpinLock 來說,它的優(yōu)勢在于等待時不會消耗 CPU 資源。
如上的代碼,如果超時時間overTime設(shè)置成>2,可完成同步操作。如果overTime<2的話,在線程1還沒有執(zhí)行完成的情況下,此時超時了,將自動執(zhí)行下面的代碼。
3、Object-C語言
3-1、使用NSLock實現(xiàn)鎖
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//[lock lock];
[lock lockBeforeDate:[NSDate date]];
NSLog(@"需要線程同步的操作1 開始");
sleep(2);
NSLog(@"需要線程同步的操作1 結(jié)束");
[lock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
if ([lock tryLock]) {//嘗試獲取鎖,如果獲取不到返回NO,不會阻塞該線程
NSLog(@"鎖可用的操作");
[lock unlock];
}else{
NSLog(@"鎖不可用的操作");
}
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
if ([lock lockBeforeDate:date]) {//嘗試在未來的3s內(nèi)獲取鎖,并阻塞該線程,如果3s內(nèi)獲取不到恢復(fù)線程, 返回NO,不會阻塞該線程
NSLog(@"沒有超時,獲得鎖");
[lock unlock];
}else{
NSLog(@"超時,沒有獲得鎖");
}
});
輸出結(jié)果
2017-11-22 14:38:37.031007+0800 test[15701:720180] 需要線程同步的操作1 開始
2017-11-22 14:38:38.033012+0800 test[15701:720178] 鎖不可用的操作
2017-11-22 14:38:39.035550+0800 test[15701:720180] 需要線程同步的操作1 結(jié)束
2017-11-22 14:38:39.036016+0800 test[15701:720178] 沒有超時,獲得鎖
NSLock是Cocoa提供給我們最基本的鎖對象,這也是我們經(jīng)常所使用的,除lock和unlock方法外,NSLock還提供了tryLock和lockBeforeDate:兩個方法,前一個方法會嘗試加鎖,如果鎖不可用(已經(jīng)被鎖住),剛并不會阻塞線程,并返回NO。lockBeforeDate:方法會在所指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。
注意:鎖定(lock)和解鎖(unLock)必須配對使用
源碼定義如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
3-2、NSRecursiveLock遞歸鎖
//NSLock *lock = [[NSLock alloc] init];
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖可以被同一線程多次請求,而不會引起死鎖。這主要是用在循環(huán)或遞歸操作中。
這段代碼如果使用注釋的NSLock *lock = [[NSLock alloc] init];而不是使用NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];會導(dǎo)致死鎖的情況。這是因為在我們的線程中,RecursiveMethod是遞歸調(diào)用的。所以每次進(jìn)入這個block時,都會去加一次鎖,而從第二次開始,由于鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞住了。調(diào)試器中會輸出如下信息:
輸出信息:
2017-11-22 16:32:05.035913+0800 test[18077:901665] value = 5
在這種情況下,我們就可以使用NSRecursiveLock。它可以允許同一線程多次加鎖,而不會造成死鎖。遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達(dá)到這種平衡,鎖最后才能被釋放,以供其它線程使用。
如果我們將NSLock代替為NSRecursiveLock,上面代碼則會正確執(zhí)行。
輸出結(jié)果:
2017-11-22 16:38:52.815599+0800 test[18430:936998] value = 5
2017-11-22 16:38:53.819971+0800 test[18430:936998] value = 4
2017-11-22 16:38:54.821962+0800 test[18430:936998] value = 3
2017-11-22 16:38:55.825592+0800 test[18430:936998] value = 2
2017-11-22 16:38:56.830927+0800 test[18430:936998] value = 1
如果需要其他功能,源碼定義如下:
@interface NSRecursiveLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
3-3、NSConditionLock條件鎖
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSMutableArray *products = [NSMutableArray array];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[lock lockWhenCondition:NO_DATA];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,總量:%zi",products.count);
[lock unlockWithCondition:HAS_DATA];
sleep(1);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"wait for product");
[lock lockWhenCondition:HAS_DATA];
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[lock unlockWithCondition:NO_DATA];
}
});
輸出結(jié)果:
2017-11-22 16:49:34.453331+0800 test[18984:990063] wait for product
2017-11-22 16:49:34.453351+0800 test[18984:990053] produce a product,總量:1
2017-11-22 16:49:34.453596+0800 test[18984:990063] custome a product
2017-11-22 16:49:34.453757+0800 test[18984:990063] wait for product
2017-11-22 16:49:35.458634+0800 test[18984:990053] produce a product,總量:1
2017-11-22 16:49:35.458813+0800 test[18984:990063] custome a product
當(dāng)我們在使用多線程的時候,有時一把只會lock和unlock的鎖未必就能完全滿足我們的使用。因為普通的鎖只能關(guān)心鎖與不鎖,而不在乎用什么鑰匙才能開鎖,而我們在處理資源共享的時候,多數(shù)情況是只有滿足一定條件的情況下才能打開這把鎖:
在線程1中的加鎖使用了lock,所以是不需要條件的,所以順利的就鎖住了,但在unlock的使用了一個整型的條件,它可以開啟其它線程中正在等待這把鑰匙的臨界地,而線程2則需要一把被標(biāo)識為2的鑰匙,所以當(dāng)線程1循環(huán)到最后一次的時候,才最終打開了線程2中的阻塞。但即便如此,NSConditionLock也跟其它的鎖一樣,是需要lock與unlock對應(yīng)的,只是lock,lockWhenCondition:與unlock,unlockWithCondition:是可以隨意組合的,當(dāng)然這是與你的需求相關(guān)的。
如果你需要其他功能,源碼定義如下:
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
3-4、NSCondition
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
if ([products count] == 0) {
NSLog(@"wait for product");
[condition wait];
}
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[condition unlock];
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,總量:%zi",products.count);
[condition signal];
[condition unlock];
sleep(1);
}
});
輸出結(jié)果:
2017-11-22 16:57:25.111774+0800 test[19391:1035560] wait for product
2017-11-22 16:57:25.112058+0800 test[19391:1035559] produce a product,總量:1
2017-11-22 16:57:25.112214+0800 test[19391:1035560] custome a product
2017-11-22 16:57:25.113221+0800 test[19391:1035560] wait for product
2017-11-22 16:57:26.117182+0800 test[19391:1035559] produce a product,總量:1
2017-11-22 16:57:26.117525+0800 test[19391:1035560] custome a product
4、C語言
4-1、pthread_mutex
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"需要線程同步的操作1 開始");
sleep(3);
NSLog(@"需要線程同步的操作1 結(jié)束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&theLock);
NSLog(@"需要線程同步的操作2");
pthread_mutex_unlock(&theLock);
});
輸出結(jié)果:
2017-11-22 17:38:29.917021+0800 test[21499:1202815] 需要線程同步的操作1 開始
2017-11-22 17:38:32.920006+0800 test[21499:1202815] 需要線程同步的操作1 結(jié)束
2017-11-22 17:38:32.920364+0800 test[21499:1202812] 需要線程同步的操作2
c語言定義下多線程加鎖方式。
1:pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr);
初始化鎖變量mutex。attr為鎖屬性,NULL值為默認(rèn)屬性。
2:pthread_mutex_lock(pthread_mutex_t mutex);加鎖
3:pthread_mutex_tylock(*pthread_mutex_t *mutex);加鎖,但是與2不一樣的是當(dāng)鎖已經(jīng)在使用的時候,返回為EBUSY,而不是掛起等待。
4:pthread_mutex_unlock(pthread_mutex_t mutex);釋放鎖
5:pthread_mutex_destroy(pthread_mutex_t mutex);使用完后釋放
4-2、pthread_mutex(recursive)
__block pthread_mutex_t theLock;
//pthread_mutex_init(&theLock, NULL);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
pthread_mutex_unlock(&theLock);
};
RecursiveMethod(5);
});
這是pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖。作用和NSRecursiveLock遞歸鎖類似。
如果使用pthread_mutex_init(&theLock, NULL);初始化鎖的話,上面的代碼會出現(xiàn)死鎖現(xiàn)象。如果使用遞歸鎖的形式,則沒有問題。
4-3、OSSpinLock
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"需要線程同步的操作1 開始");
sleep(3);
NSLog(@"需要線程同步的操作1 結(jié)束");
OSSpinLockUnlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
sleep(1);
NSLog(@"需要線程同步的操作2");
OSSpinLockUnlock(&theLock);
});
OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當(dāng)?shù)却龝r會消耗大量 CPU 資源,所以它不適用于較長時間的任務(wù)。 不過最近YY大神在自己的博客不再安全的 OSSpinLock中說明了OSSpinLock已經(jīng)不再安全,請大家謹(jǐn)慎使用。
4-4、POSIX(條件鎖)
// 實例類person
Person *person = [[Person alloc] init];
// 創(chuàng)建互斥鎖
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 創(chuàng)建條件鎖
__block pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
// 線程A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
[person personA];
pthread_mutex_unlock(&mutex);
});
// 線程B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
[person personB];
[NSThread sleepForTimeInterval:5];
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
});
效果:程序會首先調(diào)用線程B,在5秒后再調(diào)用線程A。因為在線程A中創(chuàng)建了等待條件鎖,線程B有激活鎖,只有當(dāng)線程B執(zhí)行完后會激活線程A。
pthread_cond_wait方法為等待條件鎖。
pthread_cond_signal方法為激動一個相同條件的條件鎖。
5、性能對比
對以上各個鎖進(jìn)行1000000此的加鎖解鎖的空操作時間如下:
OSSpinLock: 46.15 ms
dispatch_semaphore: 56.50 ms
pthread_mutex: 178.28 ms
NSCondition: 193.38 ms
NSLock: 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock: 157.44 ms
NSConditionLock: 490.04 ms
@synchronized: 371.17 ms
總的來說:
OSSpinLock和dispatch_semaphore的效率遠(yuǎn)遠(yuǎn)高于其他。
@synchronized和NSConditionLock效率較差。
鑒于OSSpinLock的不安全,所以我們在開發(fā)中如果考慮性能的話,建議使用dispatch_semaphore。
如果不考慮性能,只是圖個方便的話,那就使用@synchronized。
6、小結(jié)
一般來說,如果項目不大,我們都會偷點懶,直接使用關(guān)鍵字@synchronized建立鎖,懶人方法。其次可以使用蘋果提供的OC方法,最后才會去使用C去建立鎖。
相關(guān)系列文章
iOS多線程詳解(一)--- 多線程基礎(chǔ)
iOS多線程詳解(二)--- pthread&NSThread
iOS多線程詳解(三)--- GCD
iOS多線程詳解(四)--- NSOperation
iOS多線程詳解(五)--- 線程安全(鎖的創(chuàng)建)
iOS多線程詳解(六)--- 線程安全(Property)
參考文獻(xiàn)
本文主要參考了下面的鏈接,特向作者表示感謝
http://www.cnblogs.com/GarveyCalvin/p/4212611.html#synchronized
http://www.itdecent.cn/p/938d68ed832c