26.iOS底層學(xué)習(xí)之鎖synchronized

本篇提綱
1茫船、鎖的簡(jiǎn)介
2近速、鎖的性能分析
3、synchronized實(shí)現(xiàn)分析
4均牢、synchronized中的SyncData結(jié)構(gòu)
5糠雨、StripedMap的數(shù)據(jù)結(jié)構(gòu)
6、synchronized的執(zhí)行流程

1.鎖的簡(jiǎn)介

我們?cè)谑褂枚嗑€程的時(shí)候徘跪,可能會(huì)遇到多個(gè)線程同時(shí)訪問(wèn)同一個(gè)數(shù)據(jù)甘邀,導(dǎo)致數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)不安全的問(wèn)題砂竖,所以就需要使用線程同步。而最常見的線程同步的方式就是加鹃答,以保證同一時(shí)間只有同一個(gè)線程在訪問(wèn)共享數(shù)據(jù)乎澄。

2.鎖的性能分析

我們通過(guò)代碼十萬(wàn)次循環(huán),在循環(huán)中進(jìn)行加鎖测摔,解鎖的方式置济,來(lái)看一下各種鎖對(duì)循環(huán)的時(shí)間影響。下面分別是真機(jī)和锋八,模擬器運(yùn)行的結(jié)果浙于。

真機(jī)是iPhone11 iOS 15,模擬器是iPhone11 iOS 15

iPhone11
iPhone11模擬器

通過(guò)運(yùn)行結(jié)果可以看到@synchronized這種鎖挟纱,在真機(jī)和模擬器上的表現(xiàn)差別很大羞酗,真機(jī)上性能要比模擬器好一些。而@synchronized也是我們最常用的鎖紊服,這篇文章主要就來(lái)研究下@synchronized的數(shù)據(jù)結(jié)構(gòu)和內(nèi)部的具體實(shí)現(xiàn)檀轨。

3.synchronized實(shí)現(xiàn)分析

我們通過(guò)符號(hào)斷點(diǎn)的方式或者clang編譯一下,跟蹤到@synchronized對(duì)應(yīng)的代碼是這兩句:objc_sync_enter欺嗤,objc_sync_exit参萄,我們?cè)谠创a中看一下這兩個(gè)方法的具體實(shí)現(xiàn)。

  • objc_sync_enter
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不存在煎饼,那么會(huì)走objc_sync_nil 方法讹挎,進(jìn)一步看,這個(gè)方法是一個(gè)宏定義吆玖,然后是空實(shí)現(xiàn)筒溃。也就是說(shuō),如果是obj為空沾乘,就什么都不做怜奖。

  • 如果obj存在,那么會(huì)走上邊的if分支意鲸,這里邊包括了一個(gè)新的結(jié)構(gòu)體SyncData烦周,我們后邊會(huì)詳細(xì)看下它的結(jié)構(gòu)尽爆。

  • objc_sync_exit

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;
}

id2data方法在objc_sync_enterobjc_sync_exit中都有調(diào)用怎顾,而且這兩個(gè)方法中的代碼實(shí)現(xiàn)也非常的相似,都是去判斷obj漱贱,為空就什么都不做槐雾,有值就去走id2data方法,我們來(lái)具體看下這個(gè)方法的實(shí)現(xiàn)幅狮。點(diǎn)進(jìn)去發(fā)現(xiàn)大概有一百六十行左右募强,還挺多的株灸。

static SyncData* id2data(id object, enum usage why)
{
  //1、傳入object擎值,從哈希表中獲取數(shù)據(jù)
    //mutex_tt->os_unfair_lock 根據(jù)里面的文檔翻譯 是自旋鎖
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);

//傳入object慌烧,從哈希表中獲得SyncData的地址。
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

    //支持線程占存的方式
#if SUPPORT_DIRECT_THREAD_KEYS
    // 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;
          //2鸠儿、在當(dāng)前線程中的tls中尋找
            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: {
              //鎖+1
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
              //再存儲(chǔ)到tls中
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
               //鎖的個(gè)數(shù)減完之后為0了
                if (lockCount == 0) {
                    // remove from fast cache
                  //刪除局部存儲(chǔ)
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    //對(duì)SyncData對(duì)象的threadCount進(jìn)行-1屹蚊,因?yàn)楫?dāng)前線程中的對(duì)象已經(jīng)解鎖了
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

  //3、TLS中沒(méi)找到进每,在各自線程的緩存中查找
    // 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];
            //沒(méi)匹配到 跳過(guò)
            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");
            }
                
            //這個(gè)部分的執(zhí)行和在TLS中的類似
            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;
        }
    }

    // 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;
//4、遍歷syncList田晚,如果無(wú)法遍歷嘱兼,證明當(dāng)前object的list不存在,需要?jiǎng)?chuàng)建贤徒。
        for (p = *listp; p != NULL; p = p->nextData) {
            //查到了對(duì)象
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                //對(duì)threadCount+1
                OSAtomicIncrement32Barrier(&result->threadCount);
                //跳轉(zhuǎn)至done
                goto done;
            }
            
            //沒(méi)查到 記錄下object的位置
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 可以被使用的被找到了芹壕,覆蓋
        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.
    
    //創(chuàng)建一個(gè)新的SyncData對(duì)象 并且添加到syncList中
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); //內(nèi)存對(duì)齊
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
//頭插法,新增節(jié)點(diǎn)總是在頭部
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock(); //內(nèi)部的線程安全
    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) {
            // 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++;
        }
    }

    return result;
}
  • SUPPORT_DIRECT_THREAD_KEYS:支持線程占存接奈,線程占存TLS哪雕。

  • TLS,線程局部存儲(chǔ)(Thread Local Storage,TLS)鲫趁,是操作系統(tǒng)為線程單獨(dú)提供的私有空間斯嚎,通常只有有限的容量。

  • ACQUIRE在方法objc_sync_enter傳入的值挨厚,對(duì)lockCount進(jìn)行+1操作堡僻,并存儲(chǔ)。

  • RELEASE在方法objc_sync_exit傳入的值疫剃,對(duì)lockCount進(jìn)行-1操作钉疫,并進(jìn)一步判斷l(xiāng)ockCount的值是不是為0,如果為0巢价,對(duì)threadCount進(jìn)行-1操作牲阁。

  • done對(duì)list中找到的object而在TLS或者cache沒(méi)有找到的對(duì)象,進(jìn)行TLS存儲(chǔ)壤躲,或者cache存儲(chǔ)城菊,并且進(jìn)行一些錯(cuò)誤判斷。

  • 鏈表頭插法


    鏈表頭插法演示.jpg

4.synchronized中的SyncData結(jié)構(gòu)

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;
  • SyncData中又有一個(gè)struct SyncData* nextData;相同類型的指向下一個(gè)節(jié)點(diǎn)的一個(gè)next碉克,所以這是一個(gè)單向鏈表凌唬,節(jié)點(diǎn)中存儲(chǔ)了下一個(gè)節(jié)點(diǎn)的地址。
  • threadCount使用block塊的線程數(shù)
  • recursive_mutex_t遞歸鎖漏麦,底層還是os_unfair_lock客税。

5.StripedMap的數(shù)據(jù)結(jié)構(gòu)

我們通過(guò)代碼看到SyncData是從LIST_FOR_OBJ中取出來(lái)的况褪,

    SyncData **listp = &LIST_FOR_OBJ(object);

進(jìn)一步看LIST_FOR_OBJ它的定義是

#define LIST_FOR_OBJ(obj) sDataLists[obj].data

是一個(gè)宏,而sDataLists是一個(gè)靜態(tài)表

static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

StripedMap是哈希類型更耻,所以sDataLists是一張靜態(tài)哈希表测垛,內(nèi)部存儲(chǔ)SyncData,而SyncData本身又是單鏈表秧均,所以StripedMap哈希表+單鏈表的結(jié)構(gòu)赐纱。

StripedMap解決哈希沖突的方法是通過(guò)拉鏈法,就是如果計(jì)算的下標(biāo)已經(jīng)存儲(chǔ)了內(nèi)容熬北,那么會(huì)存儲(chǔ)到SyncData`的next中疙描,如果next還有內(nèi)容,會(huì)繼續(xù)往下找讶隐,直到找到可以存儲(chǔ)的位置起胰。

StripedMap結(jié)構(gòu)示意圖

結(jié)構(gòu)示意.jpg

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
};

在真機(jī)分配了8個(gè)空間,模擬器分配64個(gè)巫延。當(dāng)把模擬器修改成1后效五,不同的對(duì)象來(lái)到id2data時(shí),通過(guò)打印可以看到炉峰,當(dāng)沖突了會(huì)存到?jīng)_突位置的nextData中畏妖。

沖突處理示意

6.Synchronized的執(zhí)行流程

通過(guò)上面的討論,可以整理出以下流程疼阔。
1戒劫、調(diào)用@ synchronized(object){}時(shí),相當(dāng)于調(diào)用了方法objc_sync_enterobjc_sync_exit婆廊。
2迅细、在objc_sync_enter方法中和objc_sync_exit方法中首先都是進(jìn)行對(duì)傳入的object判斷,如果為nil就什么都不做淘邻;
如果存在茵典,那么objc_sync_enterobjc_sync_exit都會(huì)調(diào)用方法id2data只不過(guò)方法objc_sync_enter中傳的參數(shù)是ACQUIRE宾舅,而objc_sync_exit傳的是RELEASE统阿,這正好對(duì)應(yīng)了id2data方法中switch分支的處理。
3筹我、id2data中的邏輯是這樣:

  • 3.1 首先判斷是否支持TLS扶平,如果支持從TLS中查找相關(guān)的object存儲(chǔ)信息,查到了崎溃,入到switch(why)的分支判斷蜻直,如果是ACQUIRE盯质,那么鎖lockCount+1袁串,然后更新存儲(chǔ)概而,返回result
    如果是RELEASE囱修,那么鎖lockCount-1赎瑰,然后更新存儲(chǔ),再進(jìn)一步判斷l(xiāng)ockCount是不是0破镰,如果為0餐曼,threadCount-1操作,然后更新存儲(chǔ)鲜漩。

  • 3.2 如果從TLS中沒(méi)查到源譬,那么查SyncCache緩存,進(jìn)行緩存的遍歷孕似,如果查到了這個(gè)對(duì)象的緩存踩娘,進(jìn)入到switch(why)的分支判斷,如果是ACQUIRE喉祭,那么鎖lockCount+1养渴,然后更新存儲(chǔ),返回result泛烙;
    如果是RELEASE理卑,那么鎖lockCount-1,然后更新存儲(chǔ)蔽氨,再進(jìn)一步判斷l(xiāng)ockCount是不是0藐唠,如果為0,threadCount-1操作鹉究,然后更新存儲(chǔ)中捆。

  • 3.3 如果緩存也沒(méi)查到,那么去遍歷object所在的listp中查找坊饶,如果查到了泄伪,進(jìn)行threadCount的處理,并且跳轉(zhuǎn)到done匿级。done的操作是蟋滴,先進(jìn)行上面查找讀取的解鎖,然后進(jìn)行簡(jiǎn)單的錯(cuò)誤判斷痘绎。如果支持TLS津函,那么把信息更新到TLS中進(jìn)行存儲(chǔ)(這樣下次再來(lái)的時(shí)候,第一步就可以查到了)孤页,如果不支持尔苦,那么更新到cache中,下次進(jìn)來(lái)的時(shí)候第二步就可以查到了。然后返回result允坚。

  • 3.4 如果list中也沒(méi)查到魂那,那么創(chuàng)建一個(gè)新的SyncData對(duì)象 并使用頭插法插入到鏈表中(這樣下次再來(lái)到list就可以查到返回了,然后執(zhí)行l(wèi)ist往緩存存儲(chǔ)的流程)稠项,并且返回result涯雅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市展运,隨后出現(xiàn)的幾起案子活逆,更是在濱河造成了極大的恐慌,老刑警劉巖拗胜,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔗候,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡埂软,警方通過(guò)查閱死者的電腦和手機(jī)琴庵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)仰美,“玉大人迷殿,你說(shuō)我怎么就攤上這事】г樱” “怎么了庆寺?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)诉字。 經(jīng)常有香客問(wèn)我懦尝,道長(zhǎng),這世上最難降的妖魔是什么壤圃? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任陵霉,我火速辦了婚禮,結(jié)果婚禮上伍绳,老公的妹妹穿的比我還像新娘踊挠。我一直安慰自己,他們只是感情好冲杀,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布效床。 她就那樣靜靜地躺著,像睡著了一般权谁。 火紅的嫁衣襯著肌膚如雪剩檀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天旺芽,我揣著相機(jī)與錄音沪猴,去河邊找鬼辐啄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛运嗜,可吹牛的內(nèi)容都是我干的壶辜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼洗出,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼士复!你這毒婦竟也來(lái)了图谷?” 一聲冷哼從身側(cè)響起翩活,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎便贵,沒(méi)想到半個(gè)月后菠镇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡承璃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年利耍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盔粹。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隘梨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舷嗡,到底是詐尸還是另有隱情轴猎,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布进萄,位于F島的核電站捻脖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏中鼠。R本人自食惡果不足惜可婶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望援雇。 院中可真熱鬧矛渴,春花似錦、人聲如沸惫搏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)晶府。三九已至桂躏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間川陆,已是汗流浹背剂习。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鳞绕。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓失仁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親们何。 傳聞我的和親對(duì)象是個(gè)殘疾皇子萄焦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 前言 上一篇文章研究完了GCD相關(guān)的底層原理,現(xiàn)在我們開始探索鎖的底層原理冤竹。眾所周知拂封,鎖分為兩大類:自旋鎖&互斥鎖...
    冼同學(xué)閱讀 756評(píng)論 0 2
  • 一、性能分析 網(wǎng)上很多對(duì)比八大鎖性能的文章鹦蠕,時(shí)間大部分比較早冒签。蘋果對(duì)某些鎖內(nèi)部進(jìn)行了優(yōu)化。這篇文章找中會(huì)以10萬(wàn)次...
    HotPotCat閱讀 1,172評(píng)論 1 4
  • ??iOS中各種鎖性能對(duì)比钟病,建立一個(gè)10萬(wàn)次的循環(huán)萧恕,加鎖、解鎖肠阱,對(duì)比前后時(shí)間差得到其耗時(shí)時(shí)間票唆。以下是真實(shí)的測(cè)試結(jié)果...
    spyn_n閱讀 1,084評(píng)論 0 2
  • 鎖的種類 借用網(wǎng)上的一張有關(guān)鎖性能的對(duì)比圖,如下所示: 從上圖中我們可以看出來(lái)屹徘,鎖大概可以分為以下幾種: 1.:在...
    含笑州閱讀 1,008評(píng)論 0 0
  • @synchronized是比較常見的線程間同步鎖走趋,其使用相當(dāng)簡(jiǎn)單: 可在上述代碼synchronized行斷點(diǎn),...
    大成小棧閱讀 1,173評(píng)論 0 2