@synchronized 本質(zhì)是個遞歸鎖,不需要程序員手動加解鎖,并且不會產(chǎn)生死鎖問題,因此在開發(fā)中的使用頻率比較高,下面我們來研究一下他的底層實現(xiàn)。
一、底層調(diào)用實現(xiàn)
@synchronized是個關(guān)鍵字,沒法在代碼中直接跳轉(zhuǎn)查看定義,最直接的辦法就是打上斷點、看匯編:

代碼跑起來看匯編:

在匯編中,有兩個關(guān)鍵代碼,
objc_sync_enter、objc_sync_exit,他們對應(yīng)的就是加鎖和解鎖操作。
當然我們也可以通過clang(可以參考:ios 編譯調(diào)試技巧
)來看一下,整理后@synchronized對應(yīng)的代碼如下:
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
NSLog(@"-----");
} catch (id e) {
_rethrow = e;
}
{
struct _FIN {
_FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);
}
}
二、源碼實現(xiàn)
2.1 objc_sync_enter
我們這里用的是objc4-756.2的源碼,搜索objc_sync_enter找到實現(xiàn):
// 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);
assert(data);
data->mutex.lock();
} 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;
}
函數(shù)注釋:
- 開始在
obj上進行同步 - 如果需要,分配與
obj關(guān)聯(lián)的遞歸互斥體。 - 獲取鎖之后,返回
OBJC_SYNC_SUCCESS。從代碼看,返回始終未成功,這是因為當鎖被占用時,會阻塞 (data->mutex.lock();)到鎖釋放,然后往下執(zhí)行。
如果obj為空則不會加解鎖,但是被包裹在@synchronized(nil){}中的代碼塊依然會正常執(zhí)行,因為沒有阻塞當前線程。也就是說:加鎖失敗,不影響代碼繼續(xù)向下執(zhí)行。
2.2 id2data()函數(shù)實現(xiàn)
SyncData* data = id2data(obj, ACQUIRE);
data->mutex.lock();
先來了解一下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;
這個結(jié)構(gòu)體有四個對象,:
nextData:指向下一個SyncData,這看上去是一個單向鏈表。
object:對象指針,objc_object即OC對象,它保存了被鎖定對象obj(@synchronized(obj))的指針
threadCount:記錄正在使用這個代碼塊的線程數(shù)
mutex:遞歸鎖,獲取到SyncData對象后,即調(diào)用它的lock()方法
下面來看id2data()的實現(xiàn),這段代碼很長,涉及到查找緩存、對象鎖鏈表節(jié)點的創(chuàng)建插入,我們分段分析:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
// ........
return result;
}
lockp:從命名看似乎是自旋鎖,但是看關(guān)系鏈:spinlock_t -> mutex_tt<LOCKDEBUG> -> os_unfair_lock ,os_unfair_lock就是用來替代OSSpinLock這個自旋鎖的互斥鎖,文檔注釋:
@discussion
已淘汰的OSSpinLock的替代品。 不會發(fā)生爭搶,只會等待內(nèi)核被解鎖喚醒。
與OSSpinLock一樣獲取鎖是無序的,例如未鎖定者在有機會嘗試獲取鎖之前,解鎖者可能會立即重新獲取鎖。這對于性能可能是有利的,但是也可能使等待者挨餓。
listp:SyncData的二重指針,剛才知道SyncData是個鏈表,這個listp就是鏈表的頭指針
result:鎖定對象obj關(guān)聯(lián)的SyncData結(jié)構(gòu)體。
2.2.1 快速緩存
檢查當前線程單項快速緩存中是否有匹配的對象
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
fastCacheOccupied:標記線程的快速緩存是否已占用,如果找到就標記為YES,無論這個緩存是否關(guān)聯(lián)了當前要被鎖定的對象
data:當前線程的私有數(shù)據(jù),非所有數(shù)據(jù),只是鏈表中某個節(jié)點的指針
如果快速緩存中恰好是和當前對象關(guān)聯(lián)的鎖,那么對這個鎖計數(shù)+1,如果是解鎖,就-1。
快速緩存中的SyncData和鎖的計數(shù),都屬于線程的私有數(shù)據(jù),是當前線程獨有的,其他線程訪問不到。
2.2.2 線程整體緩存
線程私有數(shù)據(jù)只保存一個節(jié)點的地址,如果沒有,還要從線程的整體緩存中查找,檢查已擁有鎖的線程整體緩存中是否有匹配的對象:
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
SyncCache的結(jié)構(gòu):
typedef struct SyncCache {
//可以保存SyncCacheItem的總數(shù),已開辟的緩存空間,默認為4,2倍擴容
unsigned int allocated;
unsigned int used; //保存已使用數(shù)量
SyncCacheItem list[0]; //以及緩存鏈表的頭節(jié)點地址
} SyncCache;
SyncCacheItem的結(jié)構(gòu):
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
lockCount:當前線程持鎖計數(shù)器
如果緩存命中,加鎖則對持鎖計數(shù)器+1,如果是釋放鎖就-1;并且當持鎖計數(shù)器為0的時候,要將已使用數(shù)SyncCache->used做-1操作。
2.2.3 無緩存
如果沒有緩存,會操作使用清單listp,使用空閑節(jié)點或創(chuàng)建新節(jié)點:
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
//開始遍歷鏈表,如果找到節(jié)點中存在當前對象的鎖,goto done
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
//記錄第一個空閑節(jié)點
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
//鏈表中有空閑節(jié)點,直接征用這個節(jié)點把它和當前對象關(guān)聯(lián)起來
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
//鏈表中節(jié)點都已被使用,且沒有和當前對象相關(guān)聯(lián)的節(jié)點,那么創(chuàng)建一個新節(jié)點,并插入到鏈表的頭部
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
firstUnused:局部變量,用于記錄鏈表中第一個沒有使用的節(jié)點。由于程序中可能會對很多對象使用鎖,但是使用完了之后這個節(jié)點還在鏈表中而占用它的線程已經(jīng)沒有了,那么這個節(jié)點就可以被拿來直接用于當前的對象,省的再去開辟新的內(nèi)存空間插入鏈表,即省時又??臻g。
未找到緩存,同時有了新的SyncData節(jié)點,那么更新線程緩存:
done:
lockp->unlock();
if (result) {
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
fastCacheOccupied:1、為NO,線程快速緩存沒有被占用,則將結(jié)果保存到快速緩存;2、為YES,線程快速緩存已被占用,將節(jié)點保存到線程整體緩存中
從前面的代碼知道,只要線程快速緩存存在,無論是否命中當前需要被鎖的對象,fastCacheOccupied都會被置為YES。也就是說,線程私有數(shù)據(jù)的快速緩存只緩存第一次,且只保存第一次的這一個節(jié)點指針。
蘋果為什么這么做,個人猜想:
1、線程第一次使用同步鎖鎖定的對象,也很可能是該線程需要鎖定頻率最高的對象,比如我們經(jīng)常使用@synchronized(self)
2、該對象的同步鎖可能已經(jīng)被其他線程緩存到私有數(shù)據(jù)了,當前線程又無法訪問其他線程的私有數(shù)據(jù),如果替換的話,會重復緩存
2.2.4 listp
無緩存時從鏈表取對象,那么保存對象鎖的鏈表具體是什么樣子的呢:
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
//使用多個并行列表來減少不相關(guān)對象之間的爭用。
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
關(guān)于SyncList:
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
}
從全局靜態(tài)變量sDataLists,以obj為索引獲取到的對象類型為StripedMap<SyncList>,同時對其取地址&SyncList.data后返回。下面是StripedMap部分源碼:
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // 哈希函數(shù)
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
//.......
}
T& operator[]:C++操作符重載,通過obj取值T<SyncList>
indexForPointer:數(shù)組的索引是通過一個hash算法獲取
雖然真機的hash表大小只有8,但是這個方法取到得的是鏈表,雖然不同的對象可能取到同一個鏈表,但鏈表中有多個節(jié)點,每個節(jié)點又保存了和不同對象相關(guān)聯(lián)的鎖,這樣就避免了hash沖突

。
2.3 解鎖 objc_sync_exit()
主要通過函數(shù)id2data()獲得鎖,然后tryUnlock()。
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
三、總結(jié)
-
@synchronized()是遞歸鎖,同一線程可重入,只是內(nèi)部有個持鎖計數(shù)器而已 - 進入@synchronized()代碼塊時會執(zhí)行
objc_sync_enter(id obj)加鎖 - 核心方法是通過
id2data()來獲取到對象鎖節(jié)點SyncData
3.1. 首先從當前線程的私有數(shù)據(jù)(快速緩存)中查找
3.2. 從當前線程整體緩存中查找,檢查已擁有鎖的線程緩存中是否有匹配的對象
3.3. 從全局靜態(tài)listp對象鎖鏈表中查找,并更新線程緩存 - 退出@synchronized()代碼塊,執(zhí)行
objc_sync_enter(id obj)解鎖
lockCount:被鎖次數(shù),可遞歸重入
threadCount:SyncData影響的多線程統(tǒng)計
注意點:
在使用@synchronized(obj){}時,如果obj為nil,就不會加鎖,而代碼塊中的代碼依然會正常執(zhí)行,那就會存在風險,如下:
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (_testArray) {
_testArray = [NSMutableArray array];
}
});
}
多線程中_testArray可能會被release置nil,這個時候會加鎖失敗,同時如果發(fā)生多次release,就會crash。