Java提供了兩種內置的鎖的實現,一種是由JVM實現的synchronized和JDK提供的Lock,當你的應用是單機或者說單進程應用時,可以使用synchronized或Lock來實現鎖。
synchronized與RetreenLock區別處:Synchronized是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成,ReentrantLock類提供了一些高級功能,主要有:
- 等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。
- 公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是創建的非公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性能不是很好。
- 鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象。
Java中鎖的種類有:
- 自旋鎖:讓當前線程不停地的在循環體內執行實現的,當循環的條件被其他線程改變時 才能進入臨界區。由於自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,所以響應速度更快。但當線程數不停增加時,性能下降明顯,因為每個線程都需要執行,占用CPU時間。如果線程競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。
- 阻塞鎖:讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒,時間) 時,才可以進入線程的准備就緒狀態,准備就緒狀態的所有線程,通過競爭,進入運行狀態。
- 重入鎖:同一線程外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼。java下的ReentrantLock和synchronized都是重入鎖。
從宏觀上分為如下兩種:
- 樂觀鎖:樂觀思想,即認為讀多寫少,遇到並發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。
- 悲觀鎖:悲觀思想,即認為寫多,遇到並發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如RetreenLock。
但是,當你的應用涉及到多機、多進程共同完成時,例如現在的互聯網架構,一般都是分布式的RPC框架來支撐,那么這樣你的Server有多個,由於負載均衡的路由規則隨機,相同的請求可能會打到不同的Server上進行處理,那么這時候就需要一個全局鎖來實現多個線程(不同的進程)之間的同步。
實現全局的鎖需要依賴一個第三方系統,此系統需要滿足高可用、一致性比較強同時能應付高並發的請求。
常見的處理辦法有三種:數據庫、緩存(redis,memcached,tair)、分布式協調系統(Zookeeper)。數據庫和緩存是比較常用的,但是分布式協調系統是不常用的。
分布式鎖的設計應該是:
- 可以保證在分布式部署的應用集群中,同一個方法在同一時間只能被一台機器上的一個線程執行。
- 這把鎖要是一把可重入鎖,另外還要具備鎖失效機制(避免死鎖)
- 這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
- 有高可用的獲取鎖和釋放鎖功能
- 獲取鎖和釋放鎖的性能要好
------------------------------------------------------------------------
數據庫實現:
利用主鍵唯一規則(或者利用Mysql行鎖的特性)
首先我們利用主鍵唯一規則,在爭搶鎖的時候向DB中寫一條記錄,這條記錄主要包含鎖的id、當前占用鎖的線程名、重入的次數和創建時間等,如果插入成功表示當前線程獲取到了鎖,如果插入失敗那么證明鎖被其他人占用,等待一會兒繼續爭搶,直到爭搶到或者超時為止。
重入主要實現思路是,在每次獲取鎖之前去取當前鎖的信息,如果鎖的線程是當前線程,那么更新鎖的count+1,並且執行鎖之后的邏輯。如果不是當前鎖,那么進行重試。釋放的時候也要進行count-1,最后減到0時,刪除鎖標識釋放鎖。
優點:實現簡單
缺點:沒有超時保護機制,mysql存在單點,並發量大的時候請求量太大、沒有線程喚醒機制,用異常去控制邏輯多少優點惡心。
- 因為是基於數據庫實現的,數據庫的可用性和性能將直接影響分布式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;
- 沒有鎖失效機制,因為有可能出現成功插入數據后,服務器宕機了,對應的數據沒有被刪除,當服務恢復后一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;
- 不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。
-------------------------------------------------------------------------
緩存分布式鎖:
緩存實現分布式鎖還是比較常見的,因為緩存比較輕量,並且緩存的響應快、吞吐高。最重要的是還有自動失效的機制來保證鎖一定能釋放。
緩存的分布式鎖主要通過Redis實現,setNX是Redis提供的一個原子操作,如果指定key存在,那么setNX失敗,如果不存在會進行Set操作並返回成功。我們可以利用這個來實現一個分布式的鎖,主要思路就是,set成功表示獲取鎖,set失敗表示獲取失敗,失敗后需要重試。
優點:實現簡單,吞吐量十分客觀,對於高並發情況應付自如,自帶超時保護,對於網絡抖動的情況也可以利用超時刪除策略保證不會阻塞所有流程。
缺點:單點問題、沒有線程喚醒機制、網絡抖動可能會引起鎖刪除失敗。
---------------------------------------------------------------------------
基於zookeeper:
zookeeper是一個分布式一致性協調框架,主要可以實現選主、配置管理和分布式鎖等常用功能,因為Zookeeper的寫入都是順序的,在一個節點創建之后,其他請求再次創建便會失敗,同時可以對這個節點進行Watch,如果節點刪除會通知其他節點搶占鎖。步驟:
- 創建一個目錄mylock;
- 線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
- 獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
- 線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
- 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
Curator是Netflix開源的一套ZooKeeper客戶端框架,curator-recipes庫里面集成了很多zookeeper的應用場景,因此,需要使用zookeeper的分布式鎖功能,可以使用curator-recipes庫。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。
參考:
https://juejin.im/post/5a0be84e6fb9a0450b65ec97
https://www.jianshu.com/p/c2b4aa7a12f1