如何鎖以及分布式鎖

如何鎖以及分布式鎖,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。

為平塘等地區(qū)用戶提供了全套網頁設計制作服務,及平塘網站建設行業(yè)解決方案。主營業(yè)務為成都網站設計、成都網站建設、平塘網站設計,以傳統(tǒng)方式定制建設網站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!

在多線程的軟件世界里,對共享資源的爭搶過程(Data Race)就是并發(fā),而對共享資源數據進行訪問保護的最直接辦法就是引入鎖!。

無鎖編程也是一種辦法,但它不在本文的討論范圍,并發(fā)多線程轉為單線程(Disruptor),函數式編程,鎖粒度控制(ConcurrentHashMap桶),信號量(Semaphore)等手段都可以實現無鎖或鎖優(yōu)化。

技術上來說,鎖也可以理解成將大量并發(fā)請求串行化,但請注意串行化不能簡單等同為排隊 ,因為這里和現實世界沒什么不同,排隊意味著大家是公平Fair的領到資源,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair的去搶。Java中ReentrantLock可重入鎖,提供了公平鎖和非公平鎖兩種實現

再注意一點,串行也不是意味著只有一個排隊的隊伍,每次只能進一個。當然可以好多個隊伍,每次進入多個。比如餐館一共10個餐桌,服務員可能一次放行最多10個人進去,有人出來再放行同數量的人進去。Java中Semaphore信號量,相當于同時管理一批鎖

鎖的類型

1 自旋鎖 (Spin Lock)

自旋鎖如果已經被別的線程獲取,調用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經釋放了鎖,”自旋”一詞就是因此而得名。

自旋鎖是一種非阻塞鎖,也就是說,如果某線程需要獲取自旋鎖,但該鎖已經被其他線程占用時,該線程不會被掛起,而是在不斷的消耗CPU的時間,不停的試圖獲取自旋鎖。

Java沒有默認的自旋鎖實現,示例代碼如下:

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

通過示例,可以看到CAS原子操作將sign從期望的null設置為當前線程,線程A第一次調用lock()可以獲取鎖,第二次調用將進入循環(huán)等待,因為sign已經被設置為了current。
簡單加個當前鎖的owner比對判斷和鎖計數器,即可實現重入。

2 互斥鎖 (Mutex Lock)

互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時,該線程會被直接掛起,不再消耗CPU時間,當其他線程釋放互斥鎖后,操作系統(tǒng)會喚醒那個被掛起的線程。

阻塞鎖可以說是讓線程進入阻塞狀態(tài)進行等待,當獲得相應的信號(喚醒,時間)時,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程,通過競爭進入運行狀態(tài)。它的優(yōu)勢在于,阻塞的線程不會占用 CPU 時間, 不會導致 CPU 占用率過高,但進入時間以及恢復時間都要比自旋鎖略慢。在競爭激烈的情況下阻塞鎖的性能要明顯高于自旋鎖。

JAVA中,能夠進入/退出、阻塞狀態(tài)或包含阻塞鎖的方法有:
synchronized
ReentrantLock
Object.wait()/notify()
LockSupport.park()/unpart()(j.u.c經常使用)

自旋鎖 VS 互斥鎖
兩種鎖適用于不同場景:
如果是多核處理器,預計線程等待鎖的時間很短,短到比線程兩次上下文切換時間要少的情況下,使用自旋鎖是劃算的。

如果是多核處理器,如果預計線程等待鎖的時間較長,至少比兩次線程上下文切換的時間要長,建議使用互斥鎖。

如果是單核處理器,一般建議不要使用自旋鎖。因為,在同一時間只有一個線程是處在運行狀態(tài),那如果運行線程發(fā)現無法獲取鎖,只能等待解鎖,但因為自身不掛起,所以那個獲取到鎖的線程沒有辦法進入運行狀態(tài),只能等到運行線程把操作系統(tǒng)分給它的時間片用完,才能有機會被調度。這種情況下使用自旋鎖的代價很高。

如果加鎖的代碼經常被調用,但競爭情況很少發(fā)生時,應該優(yōu)先考慮使用自旋鎖,自旋鎖的開銷比較小,互斥量的開銷較大。

3 可重入鎖 (Reentrant Lock)

可重入鎖是一種特殊的互斥鎖,它可以被同一個線程多次獲取,而不會產生死鎖。

  1. 首先它是互斥鎖:任意時刻,只有一個線程鎖。即假設A線程已經獲取了鎖,在A線程釋放這個鎖之前,B線程是無法獲取到這個鎖的,B要獲取這個鎖就會進入阻塞狀態(tài)。

  2. 其次,它可以被同一個線程多次持有。即,假設A線程已經獲取了這個鎖,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠獲取成功的。

Java中的synchronized, ReentrantLock都是可重入鎖。

4 輕量級鎖(Lightweight Lock) & 偏向鎖(Biased Lock)

首先互斥是一種會導致線程掛起,并在較短時間內又需要重新調度回原線程的,較為消耗資源的操作。

Java6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java6里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

數據庫中針對不同的鎖層級(Lock Hierarchy,表/頁/行等),
也有類似鎖升級(Lock Escalations)的理念。

5 JUC

并發(fā)大師Doug Lea在JUC包中實現了大量的并發(fā)工具類,并發(fā)思想在源碼中得到了很好的體現。比如Semaphore, CountDownLatch, CyclicBarrier都是特定場景下的經典實現,大家有興趣可以自行研究,最終一嘆: 鎖 原來可以玩出這么多花樣來。

如何鎖以及分布式鎖

java-7-concurrent-executors-uml-class-diagram-example

鎖的后遺癥

在并發(fā)世界里,鎖扮演了一個個亦正亦邪的角色,甚至很多時候是個大反派。鎖的后遺癥包括:死鎖,饑餓,活鎖,Lock Convoying(多個同優(yōu)先級的線程重復競爭同一把鎖,此時大量雖然被喚醒而得不到鎖的線程被迫進行調度切換,這種頻繁的調度切換相當影響系統(tǒng)性能),優(yōu)先級反轉,不公平和低效率等。而這些問題都是在實現鎖的過程中普遍存在而又不得不面對的。
這里只拋出問題讓讀者了解,具體解決方案不在本文范疇。

活鎖和死鎖的區(qū)別在于,處于活鎖的實體是在不斷的改變狀態(tài),所謂之“活”, 而處于死鎖的實體表現為等待;活鎖有可能自行解開,死鎖則不能。

分布式鎖

相對于單機應用設定的單機鎖,為分布式應用各節(jié)點對共享資源的排他式訪問而設定的鎖就是分布式鎖。在分布式場景下,有很多種情況都需要實現多節(jié)點的最終一致性。比如全局發(fā)號器,分布式事務等等。

傳統(tǒng)實現分布式鎖的方案一般是利用持久化數據庫(如利用InnoDB行鎖,或事務,或version樂觀鎖),當然大部分時候可以滿足大部分人的需求。而如今互聯(lián)網應用的量級已經幾何級別的爆發(fā),利用諸如zookeeper,redis等更高效的分布式組件來實現分布式鎖,可以提供高可用的更強壯的鎖特性,并且支持豐富化的使用場景。
開源實現已有不少比如Redis作者基于Redis設計的Redlock,Redission等

小插曲:
鎖存在的地方就有爭議,Redlock也不例外。有一位分布式專家曾經發(fā)表過一片文章<How to do distributed locking>, 質疑Redlock的正確性,Redis作者則在<Is Redlock safe?>中給予了回應,爭鋒相對精彩無比,有興趣的讀者可以自行前往。

前人栽樹后人乘涼,當下各種的鎖實現已經給我們提供了很多優(yōu)雅的設計范本,我們具體來分析下分布式鎖到底應該怎么設計呢?

分布式鎖的設計要點

我們以Redis為例,簡單思考下這個鎖的實現。
似乎加鎖的時候只要一個 SETNX 命令就搞定了,返回1代表加鎖成功,返回0 表示鎖被占用著。然后再用 DEL 命令解鎖,返回1表示解鎖成功,0表示已經被解鎖過。
接著問題就來了:
SETNX會存在鎖競爭,如果在執(zhí)行過程中客戶端宕機,會引起死鎖問題,也就是鎖資源無法釋放。解決死鎖的問題其實可以可以向MySQL的死鎖檢測學習,設置一個失效時間,通過key的時間戳來判斷是否需要強制解鎖。
但是強制解鎖也存在問題,一個就是時間差問題,不同的機器的本地時間可能也存在時間差,在很小事務粒度的高并發(fā)場景下還是會存在問題,比如刪除鎖的時候,會判斷時間戳已經超過時效,有可能刪除其他已經獲取鎖的客戶端的鎖。
另外,如果設置了一個超時時間,若程序執(zhí)行時間超過了超時時間,那么還沒執(zhí)行完鎖會被自動釋放,原來持鎖的客戶端再次解鎖的時候會出現問題,而且最為嚴重的還是一致性沒有得到保障。如何合理的設置這個超時時間可能是一個觀測并不斷調整的過程。

那么,總結下設計的幾個要點:

  • 鎖的時效。避免單點故障造成死鎖,影響其他客戶端獲取鎖。但是也要保證一旦一個客戶端持鎖,在客戶端可用時不會被其他客戶端解鎖。

  • 持鎖期間的check。盡量在關鍵節(jié)點檢查鎖的狀態(tài),所以要設計成可重入鎖。

  • 減少獲取鎖的操作,盡量減少redis壓力。所以需要讓客戶端的申請鎖有一個等待時間,而不是所有申請鎖的請求線程不斷的循環(huán)申請鎖。

  • 加鎖的事務或者操作盡量粒度小,減少其他客戶端申請鎖的等待時間,提高處理效率和并發(fā)性。

  • 持鎖的客戶端解鎖后,要能通知到其他等待鎖的節(jié)點,否則其他節(jié)點只能一直等待一個預計的時間再觸發(fā)申請鎖。類似線程的notifyAll,要能同步鎖狀態(tài)給其他客戶端,并且是分布式消息。

  • 考慮任何執(zhí)行句柄中可能出現的異常,狀態(tài)的正確流轉和處理。比如,不能因為一個節(jié)點解鎖失敗,或者鎖查詢失?。╮edis 超時或者其他運行時異常),影響整個等待的任務隊列,或者任務池。

  • 若Redis服務器宕機或者網絡異常,要有其他備份方案,比如單機鎖限流+最終數據庫的持久化鎖來做好最終一致性控制。

看完上述內容,你們掌握如何鎖以及分布式鎖的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!

分享名稱:如何鎖以及分布式鎖
當前URL:http://bm7419.com/article34/iiogpe.html

成都網站建設公司_創(chuàng)新互聯(lián),為您提供手機網站建設品牌網站制作、企業(yè)建站、響應式網站網站設計公司、

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

網站建設網站維護公司