java中的CAS操作以及锁机制详解
關于CAS操作
CAS:Compare?And?Swap? ?--? 樂觀鎖策略
CAS(無鎖操作):使用CAS叫做比較交換來判斷是否出現沖突,出現沖突就重試當前操作直到不沖突為止。
悲觀鎖(JDK1.6之前的內建鎖):假設每一次執行同步代碼塊均會產生沖突,所以當線程獲取鎖成功,會阻塞其他嘗試獲取該鎖的線程。
樂觀鎖(Lock機制):假設所有線程訪問共享資源時不會出現沖突,既然不會出現沖突自然就不會阻塞其他線程。線程不會出現阻塞狀態。
1.1 CAS的操作過程
CAS的操作過程和(V,O,N)三個值有關
| ? | ? ? ? ? ? ? ? V | ? ? ? ? ? ? ? O | ? ? ? ? ? ? ? ?N |
| 具體含義 | 內存中地址存放的實際值 | 預期值(舊值) | 更新后的值 |
下面就介紹一下操作的具體過程:
CAS在最開始的時候V和O的是相等的,N中的每次線程要進行CAS操作時要新放入的值。當要進行CAS操作時,要先判斷一下V和O,若相等,說明沒有V中的值還沒有被其他線程更改,這時就可以將N中的值替換到V中。若不相等表明N中的值已經被其他的線程所更改,這時直接將N中的值返回即可;
當多個線程同時進行CAS操作時,只有一個線程會成功,并且更新V的值,其余的線程會失敗。失敗后可以選擇不斷的進行CAS操作,也可以直接掛起進行等待;
?
CAS操作與內建鎖的對比:
| 元老級的內建鎖(synchronized) | 當存在線程競爭的情況下會出現線程阻塞以及喚醒帶來的性能問題,對應互斥同步(阻塞同步),效率很低。 |
| CAS操作 | 并不會直接掛起線程,會嘗試若干次CAS操作,并非進行耗時的掛起與喚醒操作,因此非阻塞式同步。 |
1.2 引入CAS所造成的問題
1.2.1 ?關于ABA問題,如下圖所示
解決思路:沿用數據庫的樂觀鎖機制,添加版本號 1A-2B-3A
JDK1.5提供atomic包下AtomicStampedReference類來解決CAS的ABA問題
1.2.2 自旋(CAS)會浪費大量的處理器資源(CPU) 簡單來說就是太耗費時間
自旋與阻塞:舉個栗子來說,當你開車到了一個十字路口時,這時發現亮的是紅燈,那么這時的你就有兩種選擇,要么將車子直接熄火等待,要么踩住剎車等待;而這時的熄火和剎車就相當于阻塞和自旋,那么我們該如何去選擇使用哪種處理方法呢?當又回到剛才的栗子,當你到達十字路口時發現,額的神呀,今天的紅燈的等待時間竟然有半個小時,這時你二話不說將車子熄火,自己蒙頭大睡來等待紅燈;但是當你發現紅燈只有10秒鐘時,你就會選擇踩住剎車來等待紅燈;你這里的處理機制就是在不同的情況下,哪種方法使得車子耗油最少就選擇哪個方法。又回到主題上,所以不能說自旋就一定會比阻塞的性能好。
為了解決這個問題,CPU就采取了一種處理機制:自適應自旋(根據以往自旋等待時能否獲取到鎖,來動態調整自旋的時間(循環嘗試數量))
自適應自旋和出現其實也是與現實生活相關的,再次回到上個栗子中,如果之前不熄火等到了綠燈,那么這次不熄火的時間就長一點,多等會也沒事;如果之前不熄火沒等待綠燈, 那么這次不熄火的時間就短一點。自適應自旋也是如此,如果在上一次自旋時獲取到鎖,則此次自旋時間稍微變長一點;如果在上一次自旋結束還沒有獲取到鎖,此次自旋時間稍微短一點。
1.2.3 關于公平性
公平性就可以這樣理解:
| 公平模式 | 比如一個鎖被很多線程等待是時,鎖會選擇等待時間最長的線程訪問它的臨界資源,可以和隊列類比一下理解為先到先得原則(lock鎖)它就是公平的。 |
| 非公平模式 | 而當一個鎖是可以被后來的線程搶占時,它就是非公平性的,比如內建鎖(饑餓問題:由于訪問權限總是分配給了其他線程,而造成一個或多個線程被餓死的現象)。 |
自旋也是一種不公平的模式:處于阻塞狀態的線程無法立刻競爭被釋放的鎖;而處于自旋狀態的線程很有可能先獲取到鎖。
2.Java對象頭(包括標記字段和類型指針)
2.1.1 關于Mark Word
Mark Word(標記字段):用于存儲運行時對象自身的數據,相當于線程在運行時貼上自己的標簽,別的線程不可用,其占用內存大小與虛擬機位長一致,在運行期間,考慮到JVM的空間效率,Mark Word被設計成為一個非固定的數據結構,以便存儲更多有效的數據。
2.1.2 存儲運行時對象自身數據(重量級鎖JDK1.6之前)
關于Epoch:Epoch?默認最大值為40,到超過40后會變成輕量級鎖
2.1.3 關于鎖的升級
為了提高獲得鎖與釋放鎖的效率,JDK1.6之后對內建鎖做了優化(新增偏向,輕量級鎖),所以在 Java SE1.6 里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級,鎖可以升級不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率
?? ? ? ??
2.2偏向鎖
偏向鎖:最樂觀的鎖,從始至終只有一個線程請求一把鎖。
偏向鎖獲取,如下圖:
?? ? ? ? ? ? ? ? ? ? ? ??
當一個線程訪問同步代碼塊并獲取鎖時,會在對象頭的棧幀中的鎖記錄中記錄存儲偏向鎖的線程ID。以后該線程再次進入同步塊時不再需要CAS來加鎖和解鎖,只需簡單測試一下對象頭的mark?word中偏向鎖線程ID是否是當前線程ID,
如果成功,表示線程已獲取到鎖直接進入代碼塊運行。
如果測試失敗,檢查當前偏向鎖字段是否為0,如果為0,將偏向鎖字段設置為1,并且更新自己的線程ID到mark?word字段當中
如果為1,表示此時偏向鎖已經被別的線程獲取。則此線程不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,升級為輕量級鎖(升級概率較大)。
偏向鎖的撤銷:
偏向鎖使用一種等待競爭出現才釋放鎖的機制,當有其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放偏向鎖。
小tips:偏向鎖的撤銷開銷較大,需要等待線程進入全局安全點safepoint(當前線程在CPU上沒有執行任何有用字節碼)
偏向鎖從JDK1.6默認開啟,但是它在應用程序幾秒后才激活。
-XX:BiasedLockingStartupDelay=0,將延遲關閉,JVM一啟動就激活偏向鎖
-XX:-UseBiasedLocking=false,關閉偏向鎖,程序默認進入輕量級鎖。
偏向鎖的獲得和撤銷流程
2.3?輕量級鎖? 減少多線程進入互斥(mutex)的幾率
輕量級鎖:多個線程在不同時間段請求同一把鎖,也就是基本不存在鎖競爭。針對此種情況,JVM采用輕量級鎖來避免線程的阻塞以及喚醒。
只要在同一時間內有線程去競爭鎖,那么線程執行一次CAS操作,然后發現已經被別的線程搶占,直接升級為重量級鎖,不在進行CAS操作;
輕量級鎖及膨脹流程圖
加鎖:
線程在執行同步代碼塊之前,JVM先在當前線程的棧幀中創建用于存儲鎖記錄的空間,并將對象頭的mark?word字段直接復制到此空間中。然后線程嘗試使用CAS將對象頭的mark?word替換為指向鎖記錄的指針(指當前線程),如果成功表示獲取到輕量級鎖。如果失敗,表示其他線程競爭輕量級鎖,當前線程便使用自旋來不斷嘗試。
釋放:
解鎖時,會使用CAS將復制的mark?word替換回對象頭,如果成功,表示沒有競爭發生,正常解鎖。如果失敗,表示當前鎖存在競爭,進一步膨脹為重量級鎖。
三種鎖的對比:
| 鎖 | 優點 | 缺點 | 適用場景 |
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用于一個線程訪問同步代碼塊 |
| 輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度。 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 | 響應時間很短,同步塊執行速度非常快,適用于多個線程在不同時間段申請同一把鎖 |
| 重量級鎖 | 線程競爭不使用自旋,不會消耗CPU。 | 線程阻塞,響應時間緩慢。 | 追求吞吐量。 同步塊執行速度較長。 |
?
重量級鎖會阻塞,喚醒請求加鎖的線程。針對的是多個線程同一個時刻競爭同一把鎖的情況,JVM采用自適應自旋,來避免線程在面對非常小的同步塊時,仍會被阻塞以及喚醒。
輕量級鎖采用CAS操作,將鎖對象的標記字段替換為指向線程的指針,存儲著鎖對象原本的標記字段。針對的是多個線程在不同時間段申請同一把鎖的情況。
偏向鎖只會在第一次請求時采用CAS操作,在鎖對象的mark?word字段中記錄下當前線程ID,此后運行中持有偏向鎖的線程不再有加鎖過程。針對的是鎖僅會被同一線程持有
總結
以上是生活随笔為你收集整理的java中的CAS操作以及锁机制详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FastAPI 对MySQL 数据库的操
- 下一篇: Linux之管道命令