您好,登錄后才能下訂單哦!
如何鎖以及分布式鎖,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
鎖
在多線程的軟件世界里,對共享資源的爭搶過程(Data Race)就是并發,而對共享資源數據進行訪問保護的最直接辦法就是引入鎖!。
無鎖編程也是一種辦法,但它不在本文的討論范圍,并發多線程轉為單線程(Disruptor),函數式編程,鎖粒度控制(ConcurrentHashMap桶),信號量(Semaphore)等手段都可以實現無鎖或鎖優化。
技術上來說,鎖也可以理解成將大量并發請求串行化,但請注意串行化不能簡單等同為排隊 ,因為這里和現實世界沒什么不同,排隊意味著大家是公平Fair的領到資源,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair的去搶。Java中ReentrantLock可重入鎖,提供了公平鎖和非公平鎖兩種實現
再注意一點,串行也不是意味著只有一個排隊的隊伍,每次只能進一個。當然可以好多個隊伍,每次進入多個。比如餐館一共10個餐桌,服務員可能一次放行最多10個人進去,有人出來再放行同數量的人進去。Java中Semaphore信號量,相當于同時管理一批鎖
自旋鎖如果已經被別的線程獲取,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖,”自旋”一詞就是因此而得名。
自旋鎖是一種非阻塞鎖,也就是說,如果某線程需要獲取自旋鎖,但該鎖已經被其他線程占用時,該線程不會被掛起,而是在不斷的消耗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()可以獲取鎖,第二次調用將進入循環等待,因為sign已經被設置為了current。
簡單加個當前鎖的owner比對判斷和鎖計數器,即可實現重入。
互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時,該線程會被直接掛起,不再消耗CPU時間,當其他線程釋放互斥鎖后,操作系統會喚醒那個被掛起的線程。
阻塞鎖可以說是讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒,時間)時,才可以進入線程的準備就緒狀態,準備就緒狀態的所有線程,通過競爭進入運行狀態。它的優勢在于,阻塞的線程不會占用 CPU 時間, 不會導致 CPU 占用率過高,但進入時間以及恢復時間都要比自旋鎖略慢。在競爭激烈的情況下阻塞鎖的性能要明顯高于自旋鎖。
JAVA中,能夠進入/退出、阻塞狀態或包含阻塞鎖的方法有:
synchronized
ReentrantLock
Object.wait()/notify()
LockSupport.park()/unpart()(j.u.c經常使用)
自旋鎖 VS 互斥鎖
兩種鎖適用于不同場景:
如果是多核處理器,預計線程等待鎖的時間很短,短到比線程兩次上下文切換時間要少的情況下,使用自旋鎖是劃算的。如果是多核處理器,如果預計線程等待鎖的時間較長,至少比兩次線程上下文切換的時間要長,建議使用互斥鎖。
如果是單核處理器,一般建議不要使用自旋鎖。因為,在同一時間只有一個線程是處在運行狀態,那如果運行線程發現無法獲取鎖,只能等待解鎖,但因為自身不掛起,所以那個獲取到鎖的線程沒有辦法進入運行狀態,只能等到運行線程把操作系統分給它的時間片用完,才能有機會被調度。這種情況下使用自旋鎖的代價很高。
如果加鎖的代碼經常被調用,但競爭情況很少發生時,應該優先考慮使用自旋鎖,自旋鎖的開銷比較小,互斥量的開銷較大。
可重入鎖是一種特殊的互斥鎖,它可以被同一個線程多次獲取,而不會產生死鎖。
首先它是互斥鎖:任意時刻,只有一個線程鎖。即假設A線程已經獲取了鎖,在A線程釋放這個鎖之前,B線程是無法獲取到這個鎖的,B要獲取這個鎖就會進入阻塞狀態。
其次,它可以被同一個線程多次持有。即,假設A線程已經獲取了這個鎖,如果A線程在釋放鎖之前又一次請求獲取這個鎖,那么是能夠獲取成功的。
Java中的synchronized, ReentrantLock都是可重入鎖。
首先互斥是一種會導致線程掛起,并在較短時間內又需要重新調度回原線程的,較為消耗資源的操作。
Java6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
數據庫中針對不同的鎖層級(Lock Hierarchy,表/頁/行等),
也有類似鎖升級(Lock Escalations)的理念。
并發大師Doug Lea在JUC包中實現了大量的并發工具類,并發思想在源碼中得到了很好的體現。比如Semaphore, CountDownLatch, CyclicBarrier都是特定場景下的經典實現,大家有興趣可以自行研究,最終一嘆: 鎖 原來可以玩出這么多花樣來。
java-7-concurrent-executors-uml-class-diagram-example
在并發世界里,鎖扮演了一個個亦正亦邪的角色,甚至很多時候是個大反派。鎖的后遺癥包括:死鎖,饑餓,活鎖,Lock Convoying(多個同優先級的線程重復競爭同一把鎖,此時大量雖然被喚醒而得不到鎖的線程被迫進行調度切換,這種頻繁的調度切換相當影響系統性能),優先級反轉,不公平和低效率等。而這些問題都是在實現鎖的過程中普遍存在而又不得不面對的。這里只拋出問題讓讀者了解,具體解決方案不在本文范疇。
活鎖和死鎖的區別在于,處于活鎖的實體是在不斷的改變狀態,所謂之“活”, 而處于死鎖的實體表現為等待;活鎖有可能自行解開,死鎖則不能。
相對于單機應用設定的單機鎖,為分布式應用各節點對共享資源的排他式訪問而設定的鎖就是分布式鎖。在分布式場景下,有很多種情況都需要實現多節點的最終一致性。比如全局發號器,分布式事務等等。
傳統實現分布式鎖的方案一般是利用持久化數據庫(如利用InnoDB行鎖,或事務,或version樂觀鎖),當然大部分時候可以滿足大部分人的需求。而如今互聯網應用的量級已經幾何級別的爆發,利用諸如zookeeper,redis等更高效的分布式組件來實現分布式鎖,可以提供高可用的更強壯的鎖特性,并且支持豐富化的使用場景。開源實現已有不少比如Redis作者基于Redis設計的Redlock,Redission等
。
小插曲:
鎖存在的地方就有爭議,Redlock也不例外。有一位分布式專家曾經發表過一片文章<How to do distributed locking>, 質疑Redlock的正確性,Redis作者則在<Is Redlock safe?>中給予了回應,爭鋒相對精彩無比,有興趣的讀者可以自行前往。
前人栽樹后人乘涼,當下各種的鎖實現已經給我們提供了很多優雅的設計范本,我們具體來分析下分布式鎖到底應該怎么設計呢?
我們以Redis為例,簡單思考下這個鎖的實現。
似乎加鎖的時候只要一個 SETNX 命令就搞定了,返回1代表加鎖成功,返回0 表示鎖被占用著。然后再用 DEL 命令解鎖,返回1表示解鎖成功,0表示已經被解鎖過。
接著問題就來了:
SETNX會存在鎖競爭,如果在執行過程中客戶端宕機,會引起死鎖問題,也就是鎖資源無法釋放。解決死鎖的問題其實可以可以向Mysql的死鎖檢測學習,設置一個失效時間,通過key的時間戳來判斷是否需要強制解鎖。
但是強制解鎖也存在問題,一個就是時間差問題,不同的機器的本地時間可能也存在時間差,在很小事務粒度的高并發場景下還是會存在問題,比如刪除鎖的時候,會判斷時間戳已經超過時效,有可能刪除其他已經獲取鎖的客戶端的鎖。
另外,如果設置了一個超時時間,若程序執行時間超過了超時時間,那么還沒執行完鎖會被自動釋放,原來持鎖的客戶端再次解鎖的時候會出現問題,而且最為嚴重的還是一致性沒有得到保障。如何合理的設置這個超時時間可能是一個觀測并不斷調整的過程。
那么,總結下設計的幾個要點:
鎖的時效。避免單點故障造成死鎖,影響其他客戶端獲取鎖。但是也要保證一旦一個客戶端持鎖,在客戶端可用時不會被其他客戶端解鎖。
持鎖期間的check。盡量在關鍵節點檢查鎖的狀態,所以要設計成可重入鎖。
減少獲取鎖的操作,盡量減少redis壓力。所以需要讓客戶端的申請鎖有一個等待時間,而不是所有申請鎖的請求線程不斷的循環申請鎖。
加鎖的事務或者操作盡量粒度小,減少其他客戶端申請鎖的等待時間,提高處理效率和并發性。
持鎖的客戶端解鎖后,要能通知到其他等待鎖的節點,否則其他節點只能一直等待一個預計的時間再觸發申請鎖。類似線程的notifyAll,要能同步鎖狀態給其他客戶端,并且是分布式消息。
考慮任何執行句柄中可能出現的異常,狀態的正確流轉和處理。比如,不能因為一個節點解鎖失敗,或者鎖查詢失敗(redis 超時或者其他運行時異常),影響整個等待的任務隊列,或者任務池。
若Redis服務器宕機或者網絡異常,要有其他備份方案,比如單機鎖限流+最終數據庫的持久化鎖來做好最終一致性控制。
看完上述內容,你們掌握如何鎖以及分布式鎖的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。