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