java多線程(鎖機制)

1、樂觀鎖與悲觀鎖

悲觀鎖
老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。html

樂觀鎖
老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。java

兩種鎖的使用場景
從上面對兩種鎖的介紹,咱們知道兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行retry,這樣反卻是下降了性能,因此通常多寫的場景下用悲觀鎖就比較合適。redis


樂觀鎖通常會使用版本號機制或CAS算法實現。算法

1. 版本號機制
通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。數據庫

舉一個簡單的例子:
假設數據庫中賬戶信息表中有一個 version 字段,當前值爲 1 ;而當前賬戶餘額字段( balance )爲 $100 。編程

操做員 A 此時將其讀出( version=1 ),並從其賬戶餘額中扣除 50(50(100-$50 )。
在操做員 A 操做的過程當中,操做員B 也讀入此用戶信息( version=1 ),並從其賬戶餘額中扣除 20(20(100-$20 )。
操做員 A 完成了修改工做,將數據版本號加一( version=2 ),連同賬戶扣除後餘額( balance=$50 ),提交至數據庫更新,此時因爲提交數據版本大於數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新爲 2 。
操做員 B 完成了操做,也將版本號加一( version=2 )試圖向數據庫提交數據( balance=$80 ),但此時比對數據庫記錄版本時發現,操做員 B 提交的數據版本號爲 2 ,數據庫記錄當前版本也爲 2 ,不知足 「 提交版本必須大於記錄當前版本才能執行更新 「 的樂觀鎖策略,所以,操做員 B 的提交被駁回。
這樣,就避免了操做員 B 用基於 version=1 的舊數據修改的結果覆蓋操做員A 的操做結果的可能。緩存

2. CAS算法
即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的狀況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的狀況下實現變量的同步,因此也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操做數多線程

須要讀寫的內存值 V
進行比較的值 A
擬寫入的新值 B
當且僅當 V 的值等於 A時,CAS經過原子方式用新值B來更新V的值,不然不會執行任何操做(比較和替換是一個原子操做)。通常狀況下是一個自旋操做,即不斷的重試。併發

舉例:jvm

多線程狀況下如何實現count++?
使用悲觀鎖可使用synchronized對變量進行加鎖;

CAS的操做流程以下:
1.讀取內存數據j=count;

2.CAS(j,j++);即比較內存中count數據是否還爲j,若是是才進行修改;整個操做具備原子性

3.若是成功,返回;失敗則從新執行第一步直到成功,也稱之爲自旋。

因爲第二步成功的機率很大,因此採用CAS的代價很小;當高併發狀況下因爲CAS採用自旋的方式對CPU會有較大的操做負擔,因此可能會損耗部分CPU資源。

 

樂觀鎖的缺點

1 ABA 問題
若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然是A值,那咱們就能說明它的值沒有被其餘線程修改過了嗎?很明顯是不能的,由於在這段時間它的值可能被改成其餘值,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。這個問題被稱爲CAS操做的 「ABA」問題。

JDK 1.5 之後的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

2 循環時間長開銷大
自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。 若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。

3 只能保證一個共享變量的原子操做
CAS 只對單個共享變量有效,當操做涉及跨多個共享變量時 CAS 無效。可是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行 CAS 操做.因此咱們可使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操做。

參考https://blog.csdn.net/qq_34337272/article/details/81072874

https://blog.csdn.net/u010904188/article/details/87712060

2、鎖機制

有些業務邏輯在執行過程當中要求對數據進行排他性的訪問,因而須要經過一些機制保證在此過程當中數據被鎖住不會被外界修改,這就是所謂的鎖機制。

CAS是Compare And Set的縮寫,是以一種無鎖的方式實現併發控制。在實際狀況下,同時操做同一個對象的機率很是小,因此多數加鎖操做作的是無用功,CAS以一種樂觀鎖的方式實現併發控制。CAS的具體實現就是給定內存中的指望值和修改後的目標值,若是實際內存中的值等於指望值,則內存值替換爲目標值,不然操做失敗。該操做具備原子性。

悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。

樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數

重入鎖(ReentrantLock是一種遞歸無阻塞的同步機制。重入鎖,也叫作遞歸鎖,指的是同一線程 外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖。

自旋鎖,因爲自旋鎖使用者通常保持鎖時間很是短,所以選擇自旋而不是睡眠是很是必要的,自旋鎖的效率遠高於互斥鎖。如何旋轉呢?何爲自旋鎖,就是若是發現鎖定了,不是睡眠等待,而是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。

偏向鎖(Biased Locking)是Java6引入的一項多線程優化,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,這種狀況下,就會給線程加一個偏向鎖。 若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。

公平鎖,就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身

非公平鎖比較粗魯,上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。

據,可使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫若是提供相似於write_condition機制的其實都是提供的樂觀鎖。

方法鎖(synchronized修飾方法時)經過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。synchronized 方法控制對類成員變量的訪問: 每一個類實例對應一把鎖,每一個 synchronized 方法都必須得到調用該方法的類實例的鎖方能執行,不然所屬線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能得到該鎖,從新進入可執行狀態。這種機制確保了同一時刻對於每個類實例,其全部聲明爲 synchronized 的成員函數中至多隻有一個處於可執行狀態,從而有效避免了類成員變量的訪問衝突。

對象鎖(synchronized修飾方法或代碼塊)當一個對象中有synchronized method或synchronized block的時候調用此對象的同步方法或進入其同步區域時,就必須先得到對象鎖。若是此對象的對象鎖已被其餘調用者佔用,則須要等待此鎖被釋放。(方法鎖也是對象鎖)。java的全部對象都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。線程進入synchronized方法的時候獲取該對象的鎖,固然若是已經有線程獲取了這個對象的鎖,那麼當前線程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這裏也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然能夠由JVM來自動釋放。 

類鎖(synchronized修飾靜態的方法或代碼塊),因爲一個class不論被實例化多少次,其中的靜態方法和靜態變量在內存中都只有一份。因此,一旦一個靜態的方法被申明爲synchronized。此類全部的實例化對象在調用此方法,共用同一把鎖,咱們稱之爲類鎖。對象鎖是用來控制實例方法之間的同步,類鎖是用來控制靜態方法(或靜態變量互斥體)之間的同步。類鎖只是一個概念上的東西,並非真實存在的,它只是用來幫助咱們理解鎖定實例方法和靜態方法的區別的。java類可能會有不少個對象,可是隻有1個Class對象,也就是說類的不一樣實例之間共享該類的Class對象。Class對象其實也僅僅是1個java對象,只不過有點特殊而已。因爲每一個java對象都有1個互斥鎖,而類的靜態方法是須要Class對象。因此所謂的類鎖,不過是Class對象的鎖而已。獲取類的Class對象有好幾種,最簡單的就是[類名.class]的方式。

死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

死鎖發生的四個條件

  • 互斥條件:線程對資源的訪問是排他性的,若是一個線程對佔用了某資源,那麼其餘線程必須處於等待狀態,直到資源被釋放。
  • 請求和保持條件:線程T1至少已經保持了一個資源R1佔用,但又提出對另外一個資源R2請求,而此時,資源R2被其餘線程T2佔用,因而該線程T1也必須等待,但又對本身保持的資源R1不釋放。
  • 不剝奪條件:線程已得到的資源,在未使用完以前,不能被其餘線程剝奪,只能在使用完之後由本身釋放。
  • 環路等待條件:在死鎖發生時,必然存在一個「進程-資源環形鏈」,即:{p0,p1,p2,...pn},進程p0(或線程)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,因而兩個進程就相互等待)

預防死鎖,預先破壞產生死鎖的四個條件。互斥不可能破壞,因此有以下3種方法:

  • 破壞,請求和保持條件。進程等全部要請求的資源都空閒時才能申請資源,這種方法會使資源嚴重浪費(有些資源可能僅在運行初期或結束時才使用,甚至根本不使用)。容許進程獲取初期所需資源後,便開始運行,運行過程當中再逐步釋放本身佔有的資源。好比有一個進程的任務是把數據複製到磁盤中再打印,前期只須要得到磁盤資源而不須要得到打印機資源,待複製完畢後再釋放掉磁盤資源。這種方法比上一種好,會使資源利用率上升。
  • 破壞,不可搶佔條件。這種方法代價大,實現複雜
  • 破壞,循壞等待條件。對各進程請求資源的順序作一個規定,避免相互等待。這種方法對資源的利用率比前兩種都高,可是前期要爲設備指定序號,新設備加入會有一個問題,其次對用戶編程也有限制

活鎖:是指線程1可使用資源,但它很禮貌,讓其餘線程先使用資源,線程2也可使用資源,但它很紳士,也讓其餘線程先使用資源。這樣你讓我,我讓你,最後兩個線程都沒法使用資源。

死鎖與飢餓的區別

相同點:兩者都是因爲競爭資源而引發的。

不一樣點:

  • 從進程狀態考慮,死鎖進程都處於等待狀態,忙等待(處於運行或就緒狀態)的進程並不是處於等待狀態,但卻可能被餓死;
  • 死鎖進程等待永遠不會被釋放的資源,餓死進程等待會被釋放但卻不會分配給本身的資源,表現爲等待時限沒有上界(排隊等待或忙式等待);
  • 死鎖必定發生了循環等待,而餓死則否則。這也代表經過資源分配圖能夠檢測死鎖存在與否,但卻不能檢測是否有進程餓死;
  • 死鎖必定涉及多個進程,而飢餓或被餓死的進程可能只有一個。
  • 在飢餓的情形下,系統中有至少一個進程能正常運行,只是飢餓進程得不到執行機會。而死鎖則可能會最終使整個系統陷入死鎖並崩潰

怎麼檢測一個線程是否擁有鎖

java.lang.Thread中有一個方法叫holdsLock(),它返回true若是當且僅當當前線程擁有某個具體對象的鎖

3、何時應該使用可重入鎖?

場景1:若是已加鎖,則再也不重複加鎖。a、忽略重複加鎖。b、用在界面交互時點擊執行較長時間請求操做時,防止屢次點擊致使後臺重複執行(忽略重複觸發)。以上兩種狀況多用於進行非重要任務防止重複執行,(如:清除無用臨時文件,檢查某些資源的可用性,數據備份操做等)

場景2:若是發現該操做已經在執行,則嘗試等待一段時間,等待超時則不執行(嘗試等待執行)這種其實屬於場景2的改進,等待得到鎖的操做有一個時間的限制,若是超時則放棄執行。用來防止因爲資源處理不當長時間佔用致使死鎖狀況(你們都在等待資源,致使線程隊列溢出)。

場景3:若是發現該操做已經加鎖,則等待一個一個加鎖(同步執行,相似synchronized)這種比較常見你們也都在用,主要是防止資源使用衝突,保證同一時間內只有一個操做可使用該資源。但與synchronized的明顯區別是性能優點(伴隨jvm的優化這個差距在減少)。同時Lock有更靈活的鎖定方式,公平鎖與不公平鎖,而synchronized永遠是公平的。這種狀況主要用於對資源的爭搶(如:文件操做,同步消息發送,有狀態的操做等)

場景4:可中斷鎖。synchronized與Lock在默認狀況下是不會響應中斷(interrupt)操做,會繼續執行完。lockInterruptibly()提供了可中斷鎖來解決此問題。(場景3的另外一種改進,沒有超時,只能等待中斷或執行完畢)這種狀況主要用於取消某些操做對資源的佔用。如:(取消正在同步運行的操做,來防止不正常操做長時間佔用形成的阻塞)

4、如何實現分佈式鎖

基於數據庫實現分佈式鎖

基於緩存(redis,memcached,tair)實現分佈式鎖

基於Zookeeper實現分佈式鎖

參考http://www.javashuo.com/article/p-dzxeriyg-kg.html

http://www.hollischuang.com/archives/1716

相關文章
相關標籤/搜索