iOS底層-鎖的原理

鎖的種類

借用網(wǎng)上的一張有關(guān)鎖性能的對(duì)比圖,如下所示:

鎖性能對(duì)比圖.jpg

從上圖中我們可以看出來(lái),鎖大概可以分為以下幾種:

1.\color{red}{自旋鎖}:在自旋鎖中,線程會(huì)反復(fù)檢查變量是否可用。由于線程這個(gè)過程中一致保持執(zhí)行,所以是一種忙等待。 一旦獲取了自旋鎖,線程就會(huì)一直保持該鎖,直到顯式釋放自旋鎖。自旋鎖避免了進(jìn)程上下文的調(diào)度開銷,因此對(duì)于線程只會(huì)阻塞很短時(shí)間的場(chǎng)合是有效的。對(duì)于iOS屬性的修飾符atomic,自帶一把自旋鎖。

常見的有:\color{red}{OSSpinLock}、os_unfair_lock、\color{red}{atomic}

2.\color{red}{互斥鎖}:互斥鎖是一種用于多線程編程中,防止兩條線程同時(shí)對(duì)同一公共資源(例如全局變量)進(jìn)行讀寫的機(jī)制,該目的是通過將代碼切成一個(gè)個(gè)臨界區(qū)而達(dá)成。

常見的有:\color{red}{@synchronized}、\color{red}{NSLock}、pthread_mutex

3.\color{red}{條件鎖}:條件鎖就是條件變量,當(dāng)進(jìn)程的某些資源要求不滿足時(shí)就進(jìn)入休眠,即鎖住了,當(dāng)資源被分配到了,條件鎖打開了,進(jìn)程繼續(xù)運(yùn)行

常見的有:\color{red}{NSCondition}、\color{red}{NSConditionLock}

4.\color{red}{遞歸鎖}:遞歸鎖就是同一個(gè)線程可以加鎖N次而不會(huì)引發(fā)死鎖。遞歸鎖是特殊的互斥鎖,即是帶有遞歸性質(zhì)的互斥鎖

常見的有:pthread_mutex(recursive)、\color{red}{NSRecursiveLock}

5.\color{red}{信號(hào)量}:信號(hào)量是一種更高級(jí)的同步機(jī)制,互斥鎖可以說是semaphore在僅取值0/1時(shí)的特例,信號(hào)量可以有更多的取值空間,用來(lái)實(shí)現(xiàn)更加復(fù)雜的同步,而不單單是線程間互斥

常見的有:dispatch_semaphore

6.\color{red}{讀寫鎖}:讀寫鎖實(shí)際是一種特殊的自旋鎖。將對(duì)共享資源的訪問分成讀者和寫者,讀者只對(duì)共享資源進(jìn)行讀訪問,寫者則需要對(duì)共享資源進(jìn)行寫操作。這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性

  • 一個(gè)讀寫鎖同時(shí)只能有一個(gè)寫者或者多個(gè)讀者,但不能既有讀者又有寫者,在讀寫鎖保持期間也是搶占失效的

  • 如果讀寫鎖當(dāng)前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里, 直到?jīng)]有任何寫者或讀者。如果讀寫鎖沒有寫者,那么讀者可以立

其實(shí)基本的鎖就包括三類:自旋鎖、互斥鎖讀寫鎖,其他的比如條件鎖遞歸鎖、信號(hào)量都是上層的封裝和實(shí)現(xiàn)

OSSpinLock(自旋鎖)

自從OSSpinLock出現(xiàn)安全問題,在iOS10之后就被廢棄了。自旋鎖之所以不安全,是因?yàn)楂@取鎖后,線程會(huì)一直處于忙等待,造成了任務(wù)的優(yōu)先級(jí)反轉(zhuǎn)。

其中的忙等待機(jī)制可能會(huì)造成高優(yōu)先級(jí)任務(wù)一直running等待,占用時(shí)間片,而低優(yōu)先級(jí)的任務(wù)無(wú)法搶占時(shí)間片,會(huì)造成一直不能完成,鎖未釋放的情況

在OSSpinLock被棄用后,其替代方案是內(nèi)部封裝了os_unfair_lock,而os_unfair_lock在加鎖時(shí)會(huì)處于休眠狀態(tài),而不是自旋鎖的忙等狀態(tài)

synchronized(互斥遞歸鎖)探索

開啟匯編調(diào)試,發(fā)現(xiàn)@synchronized在執(zhí)行過程中,會(huì)走底層的objc_sync_enter 和 objc_sync_exit方法


image.jpg

通過對(duì)objc_sync_enter方法符號(hào)斷點(diǎn),查看底層所在的源碼庫(kù),通過斷點(diǎn)發(fā)現(xiàn)在objc源碼中,即libobjc.A.dylib

image.jpg

objc_sync_enter & objc_sync_exit 分析

進(jìn)入oc源碼查看objc_sync_enter實(shí)現(xiàn)

  • 如果obj存在,則通過id2data方法獲取相應(yīng)的SyncData,對(duì)threadCount、lockCount進(jìn)行遞增操作
  • 如果obj不存在,則調(diào)用objc_sync_nil,通過符號(hào)斷點(diǎn)得知,這個(gè)方法里面什么都沒做,直接return了
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {//傳入不為nil
        SyncData* data = id2data(obj, ACQUIRE);//重點(diǎn)
        ASSERT(data);
        data->mutex.lock();//加鎖
    } else {//傳入nil
        // @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;
}

進(jìn)入objc_sync_exit源碼實(shí)現(xiàn)

  • 如果obj存在,則調(diào)用id2data方法獲取對(duì)應(yīng)的SyncData,對(duì)threadCount、lockCount進(jìn)行遞減操作
  • 如果obj為nil,什么也不做
// End synchronizing on 'obj'. 結(jié)束對(duì)“ 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) {//obj不為nil
        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 {//obj為nil時(shí),什么也不做
        // @synchronized(nil) does nothing
    }
    return result;
}

通過上面兩個(gè)實(shí)現(xiàn)邏輯的對(duì)比,發(fā)現(xiàn)它們有一個(gè)共同點(diǎn),在obj存在時(shí),都會(huì)通過id2data方法,獲取SyncData

  • 進(jìn)入SyncData的定義,是一個(gè)結(jié)構(gòu)體,主要用來(lái)表示一個(gè)線程data,類似于鏈表結(jié)構(gòu),有next指向,且封裝了recursive_mutex_t屬性,可以確認(rèn)@synchronized確實(shí)是一個(gè)遞歸互斥鎖
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//類似鏈表結(jié)構(gòu)
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;//遞歸鎖
} SyncData;
  • 進(jìn)入SyncCache的定義,也是一個(gè)結(jié)構(gòu)體,用于存儲(chǔ)線程,其中l(wèi)ist[0]表示當(dāng)前線程的鏈表data,主要用于存儲(chǔ)SyncData和lockCount
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

id2data 分析

  • 進(jìn)入id2data源碼,從上面的分析,可以看出,這個(gè)方法是加鎖和解鎖都復(fù)用的方法
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的線程緩存)
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //通過KVC方式對(duì)線程進(jìn)行獲取 線程綁定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    //如果線程緩存中有data,執(zhí)行if流程
    if (data) {
        fastCacheOccupied = YES;
        //如果在線程空間找到了data
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            //通過KVC獲取lockCount,lockCount用來(lái)記錄 被鎖了幾次,即 該鎖可嵌套
            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: {
                //objc_sync_enter走這里,傳入的是ACQUIRE -- 獲取
                lockCount++;//通過lockCount判斷被鎖了幾次,即表示 可重入(遞歸鎖如果可重入,會(huì)死鎖)
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//設(shè)置
                break;
            }
            case RELEASE:
                //objc_sync_exit走這里,傳入的why是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;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);//判斷緩存中是否有該線程
    //如果cache中有,方式與線程緩存一致
    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中清除使用標(biāo)記
                    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;
        }
    }

    // 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.
    //第一次進(jìn)來(lái),所有緩存都找不到
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {//cache中已經(jīng)找到
            if ( p->object == object ) {//如果不等于空,且與object相似
                result = p;//賦值
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);//對(duì)threadCount進(jìn)行++
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object 沒有與當(dāng)前對(duì)象關(guān)聯(lián)的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 第一次進(jìn)來(lái),沒有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // 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));//創(chuàng)建賦值
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) { //判斷是否支持棧存緩存,支持則通過KVC形式賦值 存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
        } else 
#endif
        {
            // Save in thread cache 緩存中存一份
            if (!cache) cache = fetch_cache(YES);//第一次存儲(chǔ)時(shí),對(duì)線程進(jìn)行了綁定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

第一步、首先在tls即線程緩存中查找。

  • 在tls_get_direct方法中以線程為key,通過KVC的方式獲取與之綁定的SyncData,即線程data。其中的tls(),表示本地局部的線程緩存,

  • 判斷獲取的data是否存在,以及判斷data中是否能找到對(duì)應(yīng)的object

  • 如果都找到了,在tls_get_direct方法中以KVC的方式獲取lockCount,用來(lái)記錄對(duì)象被鎖了幾次(即鎖的嵌套次數(shù))

  • 如果data中的threadCount 小于等于0,或者 lockCount 小于等于0時(shí),則直接崩潰

  • 通過傳入的why,判斷是操作類型

如果是ACQUIRE,表示加鎖,則進(jìn)行l(wèi)ockCount++,并保存到tls緩存

如果是RELEASE,表示釋放,則進(jìn)行l(wèi)ockCount--,并保存到tls緩存。如果lockCount 等于 0,從tls中移除線程data

如果是CHECK,則什么也不做

第二步、如果tls中沒有,則在cache緩存中查找

  • 通過fetch_cache方法查找cache緩存中是否有線程

  • 如果有,則遍歷cache總表,讀取出線程對(duì)應(yīng)的SyncCacheItem

  • 從SyncCacheItem中取出data,然后后續(xù)步驟與tls的匹配是一致的

第三步、如果cache中也沒有,即第一次進(jìn)來(lái),則創(chuàng)建SyncData,并存儲(chǔ)到相應(yīng)緩存中

  • 如果在cache中找到線程,且與object相等,則進(jìn)行賦值、以及threadCount++
  • 如果在cache中沒有找到,則threadCount等于1

所以在id2data方法中,主要分為三種情況

【第一次進(jìn)來(lái),沒有鎖】:
threadCount = 1

lockCount = 1

存儲(chǔ)到tls

【不是第一次進(jìn)來(lái),且是同一個(gè)線程】
tls中有數(shù)據(jù),則lockCount++

存儲(chǔ)到tls

【不是第一次進(jìn)來(lái),且是不同線程】
全局線程空間進(jìn)行查找線程

threadCount++

lockCount++

存儲(chǔ)到cache

tls和cache表結(jié)構(gòu)

針對(duì)tls和cache緩存,底層的表結(jié)構(gòu)如下:

tls和cache緩存結(jié)構(gòu).jpg

哈希表結(jié)構(gòu)中通過SyncList結(jié)構(gòu)來(lái)組裝多線程的情況

SyncData通過鏈表的形式組裝當(dāng)前可重入的情況

下層通過tls線程緩存、cache緩存來(lái)進(jìn)行處理

底層主要有兩個(gè)東西:lockCount、threadCount,解決了遞歸互斥鎖,解決了嵌套可重入

總結(jié)

  • @synchronized在底層封裝的是一把遞歸鎖,所以這個(gè)鎖是遞歸互斥鎖

  • @synchronized的可重入,即可嵌套,主要是由于lockCount 和 threadCount的搭配

  • @synchronized使用鏈表的原因是鏈表方便下一個(gè)data的插入,但是由于底層中鏈表查詢、緩存的查找以及遞歸,是非常耗內(nèi)存以及性能的,導(dǎo)致性能低,所以在前文中,該鎖的排名在最后,但是目前該鎖的使用頻率仍然很高,主要是因?yàn)榉奖愫?jiǎn)單,且不用解鎖
    不能使用非OC對(duì)象作為加鎖對(duì)象,因?yàn)槠鋙bject的參數(shù)為id

  • @synchronized (self)這種適用于嵌套次數(shù)較少的場(chǎng)景。這里鎖住的對(duì)象也并不永遠(yuǎn)是self,這里需要讀者注意

  • 如果鎖嵌套次數(shù)較多,即鎖self過多,會(huì)導(dǎo)致底層的查找非常麻煩,因?yàn)槠涞讓邮擎湵磉M(jìn)行查找,所以會(huì)相對(duì)比較麻煩,所以此時(shí)可以使用NSLock、信號(hào)量等

NSLock 底層分析

  • 通過加符號(hào)斷點(diǎn)lock分析,發(fā)現(xiàn)其源碼在Foundation框架中


    image.jpg
  • 由于OC的Foundation框架不開源,所以這里借助Swift的開源框架Foundation來(lái) 分析NSLock的底層實(shí)現(xiàn),其原理與OC是大致相同的

image.jpg

通過源碼實(shí)現(xiàn)可以看出,底層是通過pthread_mutex互斥鎖實(shí)現(xiàn)的。并且在init方法中,還做了一些其他操作,所以在使用NSLock時(shí)需要使用init初始化
回到前文的性能圖中,可以看出NSLock的性能僅次于 pthread_mutex(互斥鎖),非常接近

** 使用弊端 **

請(qǐng)問下面block嵌套block的代碼中,會(huì)有什么問題?

for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
        };
        testMethod(10);
    });
}  
  • 在未加鎖之前,其中的current=9、10有很多條,導(dǎo)致數(shù)據(jù)混亂,主要原因是多線程導(dǎo)致的


    image.jpg
  • 如果像下面這樣加鎖,會(huì)有什么問題?

NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
        };
        testMethod(10);
        [lock unlock];
    });
}  

會(huì)出現(xiàn)一直等待的情況,主要是因?yàn)榍短资褂玫倪f歸,使用NSLock(簡(jiǎn)單的互斥鎖,如果沒有回來(lái),會(huì)一直睡覺等待),即會(huì)存在一直加lock,等不到unlock 的堵塞情況

所以,針對(duì)這種情況,可以使用以下方式解決

  • 使用@synchronized
for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            @synchronized (self) {
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
            }
        };
        testMethod(10); 
    });
}
  • 使用遞歸鎖NSRecursiveLock
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
 for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        [recursiveLock lock];
        testMethod = ^(int value){
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}

NSRecursiveLock

  • NSRecursiveLock在底層也是對(duì)pthread_mutex的封裝,可以通過swift的Foundation源碼查看


    image.jpg

對(duì)比NSLock 和 NSRecursiveLock,其底層實(shí)現(xiàn)幾乎一模一樣,區(qū)別在于init時(shí),NSRecursiveLock有一個(gè)標(biāo)識(shí)PTHREAD_MUTEX_RECURSIVE,而NSLock是默認(rèn)的

image.jpg

鎖的使用場(chǎng)景

  • 如果只是簡(jiǎn)單的使用,例如涉及線程安全,使用NSLock即可

  • 如果是循環(huán)嵌套,推薦使用@synchronized,主要是因?yàn)槭褂眠f歸鎖的 性能 不如 使用@synchronized的性能(因?yàn)樵趕ynchronized中無(wú)論怎么重入,都沒有關(guān)系,而NSRecursiveLock可能會(huì)出現(xiàn)崩潰現(xiàn)象)

  • 在循環(huán)嵌套中,如果對(duì)遞歸鎖掌握的很好,則建議使用遞歸鎖,因?yàn)樾阅芎?/p>

  • 如果是循環(huán)嵌套,并且還有多線程影響時(shí),例如有等待、死鎖現(xiàn)象時(shí),建議使用@synchronized

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概念 自旋鎖: 線程反復(fù)檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行, 因此是一種忙等待。一旦獲取了自旋鎖,線...
    MonKey_Money閱讀 1,015評(píng)論 2 1
  • iOS 底層原理 文章匯總[http://m.itdecent.cn/p/412b20d9a0f6] 本文主...
    Style_月月閱讀 4,705評(píng)論 9 16
  • 前言 之前我們分析過多線程[http://m.itdecent.cn/p/2ab8b5c4d09a],知道了...
    深圳_你要的昵稱閱讀 882評(píng)論 0 2
  • 為什么要線程同步 我們?cè)谑褂枚嗑€程的時(shí)候,可能會(huì)遇到多個(gè)線程同時(shí)訪問同一個(gè)數(shù)據(jù)導(dǎo)致數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)不安全的問題,所以...
    浪的出名閱讀 1,117評(píng)論 1 3
  • 了解鎖的機(jī)制會(huì)有助于項(xiàng)目開發(fā),從而避免項(xiàng)目中多個(gè)線程訪問同一塊資源引發(fā)數(shù)據(jù)混亂的問題。 一 概念 鎖的歸類 基本...
    yan0_0閱讀 387評(píng)論 0 2

友情鏈接更多精彩內(nèi)容