如何把握編程世界的那把鎖
1.共享變量惹得禍
我們這里是個(gè)典型的弱肉強(qiáng)食的世界, 人口多而資源少,為了爭(zhēng)搶有限的資源,大家都在自己能運(yùn)行的CPU時(shí)間片里拼了老命,經(jīng)常為了一個(gè)變量的修改而打的頭破血流。
100納秒以前, 我有幸占據(jù)了CPU,從內(nèi)存中讀取了一個(gè)變量x == 100, 我把它加了1, 休息了一會(huì)兒后我打算把它寫(xiě)回內(nèi)存, 但是驚奇的發(fā)現(xiàn): 內(nèi)存中的x 已經(jīng)變成102了。
估計(jì)是哪個(gè)不著調(diào)的線(xiàn)程在我休息的時(shí)候也讀取并且修改了x, 有不少好心的線(xiàn)程在沖我喊:不要寫(xiě)回了! 但是寫(xiě)回內(nèi)存是我的指令啊, 你不讓我執(zhí)行,難道讓我退出? 我只能毫不客氣的把101寫(xiě)入內(nèi)存, 把那個(gè)不符合我邏輯的值102給覆蓋掉, 這樣我才能執(zhí)行下一條指令。
你看,單線(xiàn)程的邏輯正確并不表示多線(xiàn)程并發(fā)運(yùn)行時(shí)的邏輯也能正確。
這樣的事情發(fā)生的多了,程序總是無(wú)法正確運(yùn)行, 引起了人類(lèi)的強(qiáng)烈不滿(mǎn),小道消息說(shuō)他們?cè)诳紤]kill掉我們, 換編程語(yǔ)言了。
但是換編程語(yǔ)言有什么用,只要有共享變量,多線(xiàn)程讀寫(xiě)的時(shí)候就是會(huì)出現(xiàn)不一致啊。
除非你消除共享變量,讓每個(gè)線(xiàn)程只訪(fǎng)問(wèn)一個(gè)函數(shù)內(nèi)的局部變量, 這些局部變量我們每個(gè)線(xiàn)程都會(huì)有一份, 函數(shù)結(jié)束以后就會(huì)銷(xiāo)毀,所以線(xiàn)程之間就隔離了,就安全了。
消除共享變量談何容易, 人類(lèi)使用的很多語(yǔ)言例如C++, Java,那些共享變量大多數(shù)一個(gè)對(duì)象的字段, 你想把字段去掉, 只留下函數(shù), 那類(lèi)也沒(méi)有存在的必要了, 就類(lèi)似于函數(shù)式編程了, 一切都是函數(shù)。 有時(shí)候我挺羨慕函數(shù)式的世界, 那種無(wú)狀態(tài)應(yīng)該是一種非常美妙的感覺(jué)吧。
2.爭(zhēng)搶吧,線(xiàn)程
既然共享變量是無(wú)法消除的,那就想想別的辦法吧, 線(xiàn)程元老院的那幫家伙們哼哧了半天,終于公布了一個(gè)方案: 加鎖!
任何線(xiàn)程,只要你想操作一個(gè)共享變量,對(duì)不起, 先去申請(qǐng)一把鎖, 拿到這把鎖才能讀取x的值 , 修改x的值, 把x寫(xiě)回內(nèi)存, ***釋放鎖,讓別人去玩。
元老院設(shè)計(jì)的這把鎖非常簡(jiǎn)單, 類(lèi)似于一個(gè)boolean 變量, boolean lock = false. 誰(shuí)能搶先把這個(gè)變量改成true, 就意味著獲取了這把鎖。
來(lái)吧,哥幾個(gè),快來(lái)?yè)尠?!
我運(yùn)行的時(shí)候, 就去檢查lock這個(gè)變量是否可以設(shè)置為true, 如果被別的家伙給搶到了(已經(jīng)變成true了), 我就在這里***循環(huán),拼命的搶?zhuān)? 除非我的時(shí)間片到了,被迫讓出CPU, 但是我不會(huì)阻塞, 還是就緒狀態(tài),等待下一次的調(diào)度, 進(jìn)入CPU繼續(xù)搶。
看到某人把它變成false, 我眼疾手快迅速出手, 終于搶到了,趕緊把lock改成true, 這把鎖現(xiàn)在屬于我了, 趕快去干活,干完活要記住把lock 改成false, 讓別的家伙們?nèi)尅?/p>
我想正是由于這種***循環(huán)的特點(diǎn), 元老院把他命名為“自旋鎖”吧!
列位看官,可能你已經(jīng)想到了, 假設(shè)有兩個(gè)線(xiàn)程,都讀到了lock == false, 都把lock 改成true, 那這個(gè)鎖算誰(shuí)的?
這個(gè)問(wèn)題元老院的大佬們?cè)缇涂紤]到了, 他們和操作系統(tǒng)(我聽(tīng)說(shuō)還有硬件)都商量好了, 這個(gè)檢測(cè)lock是否為false, 以及設(shè)置lock 為true 的操作 其實(shí)被合并了, 叫做test_and_set(lock), 操作系統(tǒng)鄭重承諾,這是一個(gè)不可分割的原子操作, 在這個(gè)test_and_set執(zhí)行的時(shí)候,總線(xiàn)都被鎖住了, 別人不能訪(fǎng)問(wèn)內(nèi)存, 即使有多個(gè)CPU在執(zhí)行也不會(huì)亂掉。
如果你感興趣,可以看看下面的實(shí)現(xiàn), 否則直接無(wú)視跳過(guò):
3.改進(jìn)
有了自旋鎖, 至少可以保證程序的正確運(yùn)行了, 我們大家都玩的不亦樂(lè)乎。
有一天我遇到了一個(gè)遞歸函數(shù), 我是挺喜歡遞歸的, 因?yàn)檫壿嫼?jiǎn)單, 只要遞歸的層次別太深, 別搞出棧溢出就好。
這個(gè)遞歸函數(shù)中需要獲得自旋鎖,做點(diǎn)事情, 然后繼續(xù)調(diào)用自己, 類(lèi)似于這樣:
我***次調(diào)用doSomething, 獲取了自旋鎖, 然后第二次調(diào)用doSomething, 還要獲取自旋鎖, 可是這個(gè)鎖已經(jīng)在我***次調(diào)用的時(shí)候持有了, 現(xiàn)在第二次調(diào)用只有***的等待了!
這下尷尬了, 我進(jìn)退不得, 自己把自己搞成了死鎖!
看來(lái)這個(gè)自旋鎖雖然能實(shí)現(xiàn)互斥的訪(fǎng)問(wèn), 但是不能重新進(jìn)入同一個(gè)函數(shù)(簡(jiǎn)稱(chēng)不可重入)啊!
我趕緊把這個(gè)問(wèn)題向元老院做了匯報(bào), 修改方案很快就下來(lái)了: 每次成功的申請(qǐng)鎖以后,要記錄下到底是誰(shuí)申請(qǐng)的, 還要用一個(gè)計(jì)數(shù)器記錄重入的次數(shù), 下一次持有鎖的家伙再次申請(qǐng)鎖只是給計(jì)數(shù)器加一而已。
釋放的時(shí)候也是一樣, 把計(jì)數(shù)器減一, 如果等于0了才真正的釋放鎖。
可重入性就這么解決了, 但是這么多線(xiàn)程都在那里拼命的搶也不是辦法, 空耗CPU也是巨大的浪費(fèi)啊。
于是元老院又發(fā)布了新的鎖 ReentrantLock, 這個(gè)鎖可以重入,如果你搶不到, 不要***循環(huán)了, 乖乖的到等待隊(duì)列里待著去, 等到鎖被別人釋放了再通知你去搶。(在Java 中最初是synchronzied關(guān)鍵字,可以用在一個(gè)方法上或者一個(gè)代碼塊上, 后來(lái)又改進(jìn)為更加靈活的ReentrantLock)
很快就有線(xiàn)程還抱怨說(shuō), 明明是我先發(fā)出獲得鎖的申請(qǐng)啊, 為什么隔壁老王卻先拿到了鎖? 這不公平啊,不行,以后得排隊(duì), 先來(lái)先得。 好吧, 只好加上一個(gè)是否公平的參數(shù)。
還有線(xiàn)程說(shuō), 我是個(gè)急性子,申請(qǐng)鎖的時(shí)候只想等待5秒鐘, 5秒之內(nèi)得不到鎖我就放棄了, 能不能支持? 那就再加上一個(gè)參數(shù):等待時(shí)間。
4.發(fā)揚(yáng)光大
體會(huì)到鎖帶來(lái)的甜頭以后, 各種各樣樣的需求紛至沓來(lái):
(1)有時(shí)候需要多個(gè)線(xiàn)程都獲得同一把鎖,去做一件事情,那怎么辦呢?
沒(méi)關(guān)系,信號(hào)量(Semaphore)出馬,創(chuàng)建信號(hào)量的時(shí)候得指定一個(gè)整數(shù)(例如10), 表明同一時(shí)刻最多有10個(gè)線(xiàn)程可以獲得鎖:
Semaphore lock= new Semaphore(10);
當(dāng)然每個(gè)線(xiàn)程都需要調(diào)用lock.aquire(), lock.release()去申請(qǐng)/釋放鎖。
(2)一個(gè)線(xiàn)程要寫(xiě)共享變量, 可是還有幾個(gè)線(xiàn)程要同時(shí)讀, 怎么辦? 你寫(xiě)的時(shí)候可以鎖住, 但總不能讀的時(shí)候也只允許一個(gè)線(xiàn)程吧?
只好來(lái)一個(gè)讀寫(xiě)鎖了ReadWriteLock, 為了保證可重入性, 元老院體貼的實(shí)現(xiàn)了ReentrantReadWriteLock。
(3)一個(gè)線(xiàn)程需要等待其他多個(gè)線(xiàn)程完工以后才能干活,怎么辦?
CountDownLatch前來(lái)救駕, 搞一個(gè)計(jì)數(shù)器,某個(gè)線(xiàn)程干完了就把計(jì)數(shù)器減去1, 如果計(jì)數(shù)器為0了,那個(gè)一直耐心等待的線(xiàn)程就可以開(kāi)始了。
(4)還有幾個(gè)線(xiàn)程必須互相等待, 就像100米賽跑那樣, 所有人都準(zhǔn)備好了才能開(kāi)閘放水, 不,是起跑, 就那就賞你一個(gè)CyclicBarrier吧。
【本文為51CTO專(zhuān)欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)coderising獲取授權(quán)】