鎖的種類
互斥鎖 自旋鎖
互斥鎖:保證在任何時候,都只有一個線程訪問對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒;
自旋鎖:與互斥鎖有點類似,只是自旋鎖 不會引起調用者睡眠,如果自旋鎖已經被別的執(zhí)行單元保持,調用者就一直循環(huán)嘗試,直到該自旋鎖的保持者已經釋放了鎖;因為不會引起調用者睡眠,所以效率高于互斥鎖;
缺點:
1、調用者在未獲得鎖的情況下,一直運行--自旋,所以占用著CPU,如果不能在很短的時間內獲得鎖,會使CPU效率降低。所以自旋鎖就主要用在臨界區(qū)持鎖時間非常短且CPU資源不緊張的情況下
2、在用自旋鎖時有可能造成死鎖,當遞歸調用時有可能造成死鎖
兩種鎖的加鎖原理
互斥鎖:線程會從sleep(加鎖)——>running(解鎖),過程中有上下文的切換,cpu的搶占,信號的發(fā)送等開銷。
自旋鎖:線程一直是running(加鎖——>解鎖),死循環(huán)檢測鎖的標志位,機制不復雜。
遞歸鎖
特殊的互斥鎖
iOS中的鎖
1.@synchronized
@synchronized (self) {
要鎖的代碼
}
打斷點使用匯編查看內部實現(xiàn),可看到被執(zhí)行的代碼會被下文中的兩句代碼包裹
objc_sync_enter
要鎖的代碼
objc_sync_exit
在objc中源碼查看
######### objc_sync_enter
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {//判斷對象是否存在
SyncData* data = id2data(obj, ACQUIRE);//從表中取出需要鎖的數(shù)據
assert(data);
data->mutex.lock();//對數(shù)據加鎖
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil(); //如果對象不存在,什么事情都不做!
}
return result;
}
######### SyncData
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex; //遞歸鎖
} SyncData;
@synchronized結論:
- 是對互斥鎖的一種封裝
- 具體點是種特殊的互斥鎖->遞歸鎖,內部搭配
nil防止死鎖 - 通過
表的結構存要鎖的對象 - 表內部的對象又是通過哈希存儲的
坑點:在大量線程異步同時操作同一個對象時,因為遞歸鎖會不停的alloc/release,在某一個對象會是nil;而此時 @synchronized (obj) 會判斷obj==nil,就不會再加鎖,導致線程訪問沖突;eg
#import "KTest.h"
@interface KTest()
@property (nonatomic,strong) NSMutableArray *testArray;
@end
@implementation KTest
- (void)crash {
//_testArray
//nil 不加鎖 - old release
//hash objc - nil
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (_testArray) {//在某一刻_testArray= nil,導致加鎖失敗
_testArray = [NSMutableArray array];
}
});
}
}
//解決NSLock
- (void)NO_crash {
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 20000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
_testArray = [NSMutableArray array];
[lock unlock];
});
}
}
@end
2.NSLock
在上面的例子里我們用NSLock去解決在大量線程異步同時操作同一個對象的內存安全問題;那我們細看下NSLock的源碼,NSLock屬于Foundation,需要在Foundation中查找,我這里是Swift版本的Foundation,我對源碼做了一些簡化方便查看
open class NSLock: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
public override init() {
pthread_mutex_init(mutex, nil)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
}
deinit {
pthread_mutex_destroy(mutex)
mutex.deinitialize(count: 1)
mutex.deallocate()
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
}
open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}
open func lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
}
open var name: String?
}
上面源碼可看出:
- NSLock是對pthread_mutex的封裝
- NSLock還有timeout超時控制
坑點:當NSLock對同一個線程鎖兩次,就會造成死鎖;即不能實現(xiàn)遞歸鎖,這種情況需要用到NSRecursiveLock先看官方文檔上的話
NSLock
遞歸調用示例:
//NSLock
- (void)NSLock_crash {
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testBlock)(int);
testBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value-->%d",value);
testBlock(value-1);//遞歸調用,用遞歸鎖
}
[lock unlock];
};
testBlock(10);
});
}
//遞歸鎖NSRecursiveLock
- (void)NSRecursiveLock_NO_crash {
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testBlock)(int);
testBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value-->%d",value);
testBlock(value-1);//遞歸調用,用遞歸鎖
}
[lock unlock];
};
testBlock(10);
});
}
3.NSRecursiveLock
在上面的例子已經說明了NSRecursiveLock能夠處理遞歸調用;但是還是要看看源碼
open class NSRecursiveLock: NSObject, NSLocking {
internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
public override init() {
super.init()
var attrib = pthread_mutexattr_t()
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
}
deinit {
pthread_mutex_destroy(mutex)
mutex.deinitialize(count: 1)
mutex.deallocate()
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
}
open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}
open func lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
}
open var name: String?
}
上面源碼可看出:
- NSRecursiveLock也是對pthread_mutex的封裝,不同的是加Recursive遞歸調用功能;
- NSRecursiveLock同樣也有timeout超時控制
4. NSCondition
相對來說NSCondition用的比較少,但也需要了解。先看源碼
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init() {
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
}
deinit {
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
open func wait(until limit: Date) -> Bool {
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
open func signal() {
pthread_cond_signal(cond)
}
open func broadcast() {
// 匯編分析 - 猜 (多看多玩)
pthread_cond_broadcast(cond) // wait signal
}
open var name: String?
}
由上面源碼可以看出:
- NSCondition 也是對pthread_mutex的封裝
- 使用wait信號可以讓當前線程處于等待中
- 使用signal信號可以告訴其他某一個線程不用再等待了,可以繼續(xù)執(zhí)行
- 內部還有一個broadcast(廣播)信號,用于發(fā)送(signal)信號給其他所有線程
用法:
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (!array.count) {
[lock wait];
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保證讓線程2的代碼后執(zhí)行
[lock lock];
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
[lock unlock];
});
5.NSConditionLock類似于信號量
先看源碼
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
_thread = nil
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
_thread = nil
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open var name: String?
}
由源碼可知:
- NSConditionLock 是 對NSCondition+線程數(shù)的封裝,即NSConditionLock = NSCondition + lock
-
internal var _thread: _swift_CFThreadRef?:_thread就是當前可以同事操作的線程數(shù),通過搭配NSCondition可以達到dispatch_semaphore的效果 -
lock(before: Date.distantFuture):也有超時時間
用法示例:
#pragma mark -- NSConditionLock
- (void)testConditonLock{
// 信號量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"線程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"線程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"線程 3");
[conditionLock unlock];
});
}
dispatch_semaphore
dispatch_semaphore 是 GCD 用來同步的一種方式,與他相關的只有三個函數(shù),一個是創(chuàng)建信號量,一個是等待信號,一個是發(fā)送信號。
dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
dispatch_semaphore 和 NSConditionLock 類似,都是一種基于信號的同步方式,但 NSCondition 信號只能發(fā)送,不能保存(如果沒有線程在等待,則發(fā)送的信號會失效)。而 dispatch_semaphore 能保存發(fā)送的信號。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號量。
dispatch_semaphore_create(1) 方法可以創(chuàng)建一個 dispatch_semaphore_t 類型的信號量,設定信號量的初始值為 1。注意,這里的傳入的參數(shù)必須大于或等于 0,否則 dispatch_semaphore_create 會返回 NULL。
dispatch_semaphore_wait(signal, overTime); 方法會判斷 signal 的信號值是否大于 0。大于 0 不會阻塞線程,消耗掉一個信號,執(zhí)行后續(xù)任務。如果信號值為 0,該線程會和 NSCondition 一樣直接進入 waiting 狀態(tài),等待其他線程發(fā)送信號喚醒線程去執(zhí)行后續(xù)任務,或者當 overTime 時限到了,也會執(zhí)行后續(xù)任務。
dispatch_semaphore_signal(signal); 發(fā)送信號,如果沒有等待的線程接受信號,則使 signal 信號值加一(做到對信號的保存)。
用法:
- (void)testDispatch_semaphore_t {
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);
sleep(2);
NSLog(@"線程1");
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);
});
}
從上面的實例代碼可以看到,一個 dispatch_semaphore_wait(signal, overTime); 方法會去對應一個 dispatch_semaphore_signal(signal); 看起來像 NSLock 的 lock 和 unlock,其實可以這樣理解,區(qū)別只在于有信號量這個參數(shù),lock unlock 只能同一時間,一個線程訪問被保護的臨界區(qū),而如果 dispatch_semaphore 的信號量初始值為 x ,則可以有 x 個線程同時訪問被保護的臨界區(qū)。
7.OSSpinLock - os_unfair_lock
在iOS10 之前,OSSpinLock 是一種自旋鎖,也只有加鎖,解鎖,嘗試加鎖三個方法。和 NSLock 不同的是 NSLock 請求加鎖失敗的話,會先輪詢,但一秒過后便會使線程進入 waiting 狀態(tài),等待喚醒。而 OSSpinLock 會一直輪詢,等待時會消耗大量 CPU 資源,不適用于較長時間的任務。而因為OSSpinLock不再線程安全,在iOS10之后OSSpinLock被廢棄內部封裝了os_unfair_lock,os_unfair_lock也是一種互斥鎖不會忙等。
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
bool OSSpinLockTry( volatile OSSpinLock *__lock );
void OSSpinLockLock( volatile OSSpinLock *__lock );
void OSSpinLockUnlock( volatile OSSpinLock *__lock );
用例示范:
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"線程1");
sleep(10);
OSSpinLockUnlock(&theLock);
NSLog(@"線程1解鎖成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
OSSpinLockLock(&theLock);
NSLog(@"線程2");
OSSpinLockUnlock(&theLock);
});
2016-08-19 20:25:13.526 ThreadLockControlDemo[2856:316247] 線程1
2016-08-19 20:25:23.528 ThreadLockControlDemo[2856:316247] 線程1解鎖成功
2016-08-19 20:25:23.529 ThreadLockControlDemo[2856:316260] 線程2
7.讀寫鎖
讀寫鎖是一種特殊的的自旋鎖;它能做到多讀單寫;
實現(xiàn): 并發(fā)隊列 + dispatch_barrier_async
########### .h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RF_RWLock : NSObject
// 讀數(shù)據
- (id)rf_objectForKey:(NSString *)key;
// 寫數(shù)據
- (void)rf_setObject:(id)obj forKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
########### .m文件
#import "RF_RWLock.h"
@interface RF_RWLock ()
// 定義一個并發(fā)隊列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用戶數(shù)據中心, 可能多個線程需要數(shù)據訪問:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end
@implementation RF_RWLock
- (id)init{
self = [super init];
if (self){
// 創(chuàng)建一個并發(fā)隊列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 創(chuàng)建數(shù)據字典:
self.dataCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark - 讀數(shù)據
- (id)rf_objectForKey:(NSString *)key{
__block id obj;
// 同步讀取指定數(shù)據:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataCenterDic objectForKey:key];
});
return obj;
}
#pragma mark - 寫數(shù)據
- (void)rf_setObject:(id)obj forKey:(NSString *)key{
// 異步柵欄調用設置數(shù)據:
dispatch_barrier_async(self.concurrent_queue, ^{
[self.dataCenterDic setObject:obj forKey:key];
});
}
@end
番外 -- 面試題
atomic
1.atomic的原理?
atomic 在對象get/set的時候,會有一個spinlock_t控制。即當兩個線程A和B,如果A正在執(zhí)行getter時,B如果想要執(zhí)行settet,就要等A執(zhí)行getter完成后才能執(zhí)行
2.atomic修飾的屬性絕對安全嗎?
-
atomic只保證set/get方法安全,但是當多個線程不使用set/get方法訪問時,就不再安全; - 所以
atomic屬性和property的多線程安全并沒有什么直接的聯(lián)系,多線程安全還是要程序員自己保障 -
atomic的由于使用了自旋鎖,性能比nonatomic慢20倍
參考 iOS 中的八大鎖
