鎖
分布式鎖
distributed locks
資源有限锯蛀,爭搶難免什猖,最簡單粗暴的辦法是誰的拳頭大誰就可以搶到最好的資源薇组。那在計算機世界里是如何搶奪資源(cpu, 內存, 網(wǎng)絡等)的呢肿仑?
鎖
在多線程的軟件世界里序目,對共享資源的爭搶過程(Data Race)就是并發(fā)坑律,而對共享資源數(shù)據(jù)進行訪問保護的最直接辦法就是引入鎖岩梳!。
POSIX threads(簡稱Pthreads)是在多核平臺上進行并行編程的一套常用的API晃择。線程同步(Thread Synchronization)是并行編程中非常重要的通訊手段冀值,其中最典型的應用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(qū)(Critical Section)進行保護(另一種常用的同步機制是barrier)。
無鎖編程也是一種辦法宫屠,但它不在本文的討論范圍列疗,并發(fā)多線程轉為單線程(Disruptor),函數(shù)式編程浪蹂,鎖粒度控制(ConcurrentHashMap桶)抵栈,信號量(Semaphore)等手段都可以實現(xiàn)無鎖或鎖優(yōu)化。
技術上來說坤次,鎖也可以理解成將大量并發(fā)請求串行化古劲,但請注意串行化不能簡單等同為** 排隊 ,因為這里和現(xiàn)實世界沒什么不同缰猴,排隊意味著大家是公平Fair的領到資源产艾,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair**的去搶。Java中ReentrantLock可重入鎖闷堡,提供了公平鎖和非公平鎖兩種實現(xiàn)
再注意一點隘膘,串行也不是意味著只有一個排隊的隊伍,每次只能進一個缚窿。當然可以好多個隊伍棘幸,每次進入多個焰扳。比如餐館一共10個餐桌倦零,服務員可能一次放行最多10個人進去,有人出來再放行同數(shù)量的人進去吨悍。Java中Semaphore信號量扫茅,相當于同時管理一批鎖
鎖的類型
1 自旋鎖 (Spin Lock)
自旋鎖如果已經(jīng)被別的線程獲取,調用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖育瓜,”自旋”一詞就是因此而得名葫隙。
自旋鎖是一種非阻塞鎖,也就是說躏仇,如果某線程需要獲取自旋鎖恋脚,但該鎖已經(jīng)被其他線程占用時,該線程不會被掛起焰手,而是在不斷的消耗CPU的時間糟描,不停的試圖獲取自旋鎖。
Java沒有默認的自旋鎖實現(xiàn)书妻,示例代碼如下:
<pre>
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
</pre>
通過示例船响,可以看到CAS原子操作將sign從期望的null設置為當前線程,線程A第一次調用lock()可以獲取鎖躲履,第二次調用將進入循環(huán)等待见间,因為sign已經(jīng)被設置為了current。
簡單加個當前鎖的owner比對判斷和鎖計數(shù)器工猜,即可實現(xiàn)重入米诉。
2 互斥鎖 (Mutex Lock)
互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時篷帅,該線程會被直接掛起荒辕,不再消耗CPU時間,當其他線程釋放互斥鎖后犹褒,操作系統(tǒng)會喚醒那個被掛起的線程抵窒。
阻塞鎖可以說是讓線程進入阻塞狀態(tài)進行等待,當獲得相應的信號(喚醒叠骑,時間)時李皇,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程,通過競爭進入運行狀態(tài)掉房。它的優(yōu)勢在于茧跋,阻塞的線程不會占用 CPU 時間, 不會導致 CPU 占用率過高卓囚,但進入時間以及恢復時間都要比自旋鎖略慢瘾杭。在競爭激烈的情況下阻塞鎖的性能要明顯高于自旋鎖。
<pre>
JAVA中哪亿,能夠進入/退出粥烁、阻塞狀態(tài)或包含阻塞鎖的方法有:
synchronized
ReentrantLock
Object.wait()/notify()
LockSupport.park()/unpart()(j.u.c經(jīng)常使用)
</pre>
自旋鎖 VS 互斥鎖
兩種鎖適用于不同場景:
如果是多核處理器,預計線程等待鎖的時間很短蝇棉,短到比線程兩次上下文切換時間要少的情況下讨阻,使用自旋鎖是劃算的。
如果是多核處理器篡殷,如果預計線程等待鎖的時間較長钝吮,至少比兩次線程上下文切換的時間要長,建議使用互斥鎖板辽。
如果是單核處理器奇瘦,一般建議不要使用自旋鎖。因為劲弦,在同一時間只有一個線程是處在運行狀態(tài)耳标,那如果運行線程發(fā)現(xiàn)無法獲取鎖,只能等待解鎖瓶您,但因為自身不掛起麻捻,所以那個獲取到鎖的線程沒有辦法進入運行狀態(tài),只能等到運行線程把操作系統(tǒng)分給它的時間片用完呀袱,才能有機會被調度贸毕。這種情況下使用自旋鎖的代價很高。
如果加鎖的代碼經(jīng)常被調用夜赵,但競爭情況很少發(fā)生時明棍,應該優(yōu)先考慮使用自旋鎖,自旋鎖的開銷比較小寇僧,互斥量的開銷較大摊腋。
3 可重入鎖 (Reentrant Lock)
可重入鎖是一種特殊的互斥鎖,它可以被同一個線程多次獲取嘁傀,而不會產(chǎn)生死鎖兴蒸。
- 首先它是互斥鎖:任意時刻,只有一個線程鎖细办。即假設A線程已經(jīng)獲取了鎖橙凳,在A線程釋放這個鎖之前,B線程是無法獲取到這個鎖的,B要獲取這個鎖就會進入阻塞狀態(tài)岛啸。
- 其次钓觉,它可以被同一個線程多次持有。即坚踩,假設A線程已經(jīng)獲取了這個鎖荡灾,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠獲取成功的瞬铸。
<pre>Java中的synchronized, ReentrantLock都是可重入鎖批幌。</pre>
4 輕量級鎖(Lightweight Lock) & 偏向鎖(Biased Lock)
首先互斥是一種會導致線程掛起,并在較短時間內又需要重新調度回原線程的赴捞,較為消耗資源的操作逼裆。
Java6為了減少獲得鎖和釋放鎖所帶來的性能消耗郁稍,引入了“偏向鎖”和“輕量級鎖”赦政,所以在Java6里鎖一共有四種狀態(tài),無鎖狀態(tài)耀怜,偏向鎖狀態(tài)恢着,輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級财破。鎖可以升級但不能降級掰派,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略左痢,目的是為了提高獲得鎖和釋放鎖的效率靡羡。
<pre>數(shù)據(jù)庫中針對不同的鎖層級(Lock Hierarchy,表/頁/行等)俊性,
也有類似鎖升級(Lock Escalations)的理念略步。</pre>
5 JUC
并發(fā)大師Doug Lea在JUC包中實現(xiàn)了大量的并發(fā)工具類,并發(fā)思想在源碼中得到了很好的體現(xiàn)定页。比如Semaphore, CountDownLatch, CyclicBarrier都是特定場景下的經(jīng)典實現(xiàn)趟薄,大家有興趣可以自行研究,最終一嘆:** 鎖 **原來可以玩出這么多花樣來典徊。
鎖的后遺癥
在并發(fā)世界里杭煎,鎖扮演了一個個亦正亦邪的角色,甚至很多時候是個大反派卒落。鎖的后遺癥包括:死鎖羡铲,饑餓,活鎖儡毕,Lock Convoying(多個同優(yōu)先級的線程重復競爭同一把鎖也切,此時大量雖然被喚醒而得不到鎖的線程被迫進行調度切換,這種頻繁的調度切換相當影響系統(tǒng)性能),優(yōu)先級反轉贾费,不公平和低效率等钦购。而這些問題都是在實現(xiàn)鎖的過程中普遍存在而又不得不面對的。
這里只拋出問題讓讀者了解褂萧,具體解決方案不在本文范疇押桃。
活鎖和死鎖的區(qū)別在于,處于活鎖的實體是在不斷的改變狀態(tài)导犹,所謂之“活”唱凯, 而處于死鎖的實體表現(xiàn)為等待;活鎖有可能自行解開谎痢,死鎖則不能磕昼。
分布式鎖
相對于單機應用設定的單機鎖,為分布式應用各節(jié)點對共享資源的排他式訪問而設定的鎖就是分布式鎖节猿。在分布式場景下票从,有很多種情況都需要實現(xiàn)多節(jié)點的最終一致性。比如全局發(fā)號器滨嘱,分布式事務等等峰鄙。
傳統(tǒng)實現(xiàn)分布式鎖的方案一般是利用持久化數(shù)據(jù)庫(如利用InnoDB行鎖,或事務太雨,或version樂觀鎖)吟榴,當然大部分時候可以滿足大部分人的需求。而如今互聯(lián)網(wǎng)應用的量級已經(jīng)幾何級別的爆發(fā)囊扳,利用諸如zookeeper吩翻,redis等更高效的分布式組件來實現(xiàn)分布式鎖,可以提供高可用的更強壯的鎖特性锥咸,并且支持豐富化的使用場景狭瞎。
開源實現(xiàn)已有不少比如Redis作者基于Redis設計的Redlock,Redission等
她君。
小插曲:
鎖存在的地方就有爭議脚作,Redlock也不例外。有一位分布式專家曾經(jīng)發(fā)表過一片文章<How to do distributed locking>, 質疑Redlock的正確性缔刹,Redis作者則在<Is Redlock safe?>中給予了回應球涛,爭鋒相對精彩無比,有興趣的讀者可以自行前往校镐。
前人栽樹后人乘涼亿扁,當下各種的鎖實現(xiàn)已經(jīng)給我們提供了很多優(yōu)雅的設計范本,我們具體來分析下分布式鎖到底應該怎么設計呢鸟廓?
分布式鎖的設計要點
我們以Redis為例从祝,簡單思考下這個鎖的實現(xiàn)襟己。
似乎加鎖的時候只要一個*** SETNX 命令就搞定了,返回1代表加鎖成功牍陌,返回0 表示鎖被占用著擎浴。然后再用 DEL ***命令解鎖,返回1表示解鎖成功毒涧,0表示已經(jīng)被解鎖過贮预。
接著問題就來了:
SETNX會存在鎖競爭,如果在執(zhí)行過程中客戶端宕機契讲,會引起死鎖問題仿吞,也就是鎖資源無法釋放。解決死鎖的問題其實可以可以向Mysql的死鎖檢測學習捡偏,設置一個失效時間唤冈,通過key的時間戳來判斷是否需要強制解鎖。
但是強制解鎖也存在問題银伟,一個就是時間差問題你虹,不同的機器的本地時間可能也存在時間差,在很小事務粒度的高并發(fā)場景下還是會存在問題枣申,比如刪除鎖的時候售葡,會判斷時間戳已經(jīng)超過時效看杭,有可能刪除其他已經(jīng)獲取鎖的客戶端的鎖忠藤。
另外,如果設置了一個超時時間楼雹,若程序執(zhí)行時間超過了超時時間模孩,那么還沒執(zhí)行完鎖會被自動釋放,原來持鎖的客戶端再次解鎖的時候會出現(xiàn)問題贮缅,而且最為嚴重的還是一致性沒有得到保障榨咐。如何合理的設置這個超時時間可能是一個觀測并不斷調整的過程。
那么谴供,總結下設計的幾個要點:
- 鎖的時效块茁。避免單點故障造成死鎖,影響其他客戶端獲取鎖桂肌。但是也要保證一旦一個客戶端持鎖数焊,在客戶端可用時不會被其他客戶端解鎖。
- 持鎖期間的check崎场。盡量在關鍵節(jié)點檢查鎖的狀態(tài)佩耳,所以要設計成可重入鎖。
- 減少獲取鎖的操作谭跨,盡量減少redis壓力干厚。所以需要讓客戶端的申請鎖有一個等待時間李滴,而不是所有申請鎖的請求線程不斷的循環(huán)申請鎖。
- 加鎖的事務或者操作盡量粒度小蛮瞄,減少其他客戶端申請鎖的等待時間所坯,提高處理效率和并發(fā)性。
- 持鎖的客戶端解鎖后挂捅,要能通知到其他等待鎖的節(jié)點包竹,否則其他節(jié)點只能一直等待一個預計的時間再觸發(fā)申請鎖。類似線程的notifyAll,要能同步鎖狀態(tài)給其他客戶端籍凝,并且是分布式消息周瞎。
- 考慮任何執(zhí)行句柄中可能出現(xiàn)的異常,狀態(tài)的正確流轉和處理饵蒂。比如声诸,不能因為一個節(jié)點解鎖失敗,或者鎖查詢失斖硕ⅰ(redis 超時或者其他運行時異常)彼乌,影響整個等待的任務隊列,或者任務池渊迁。
- 若Redis服務器宕機或者網(wǎng)絡異常慰照,要有其他備份方案,比如單機鎖限流+最終數(shù)據(jù)庫的持久化鎖來做好最終一致性控制琉朽。
如果大家想自己實現(xiàn)分布式鎖的話毒租,建議先把開源的一些實現(xiàn)先讀一讀,拓展下思路箱叁。
還是那句話墅垮,如非必要,勿增實體耕漱!