【JUC系列】Java的锁机制
偏向鎖/輕量級鎖/重量級鎖
????????重量級鎖會造成 CPU 在用戶態和核心態之間頻繁切換,所以代價高、效率低。JDK1.6 版本為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”實現。所以,在 JDK1.6 版本里內置鎖一共有四種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,這些狀態隨著競爭情況逐漸升級。內置鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種能升級卻不能降級的策略,其目的是為了提高獲得鎖和釋放鎖的效率。
1. 無鎖狀態
Java 對象剛創建時,還沒有任何線程來競爭,說明該對象處于無鎖狀態(無線程競爭它)2. 偏向鎖狀態
偏向鎖是指一段同步代碼一直被一同個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖 的代價。如果內置鎖處于偏向狀態,當有一個線程來競爭鎖時,先用偏向鎖,表示內置鎖偏愛這 個線程,這個線程要執行該鎖關聯的同步代碼時,不需要再做任何檢查和切換。偏向鎖在競爭不 激烈的情況下,效率非常高。3. 輕量級鎖狀態
????????當有兩個線程開始競爭這個鎖對象,情況發生變化了,不再是偏向(獨占)鎖了,鎖會升級 為輕量級鎖,兩個線程公平競爭。當鎖處于偏向鎖的時候,而又被另一個線程所企圖搶占時,偏向鎖就會升級為輕量級鎖。企圖搶占的線程會通過自旋的形式嘗試獲取鎖,不會阻塞搶鎖線程,以便提高性能。 ????????自旋原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的 線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等 持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。 但是線程自旋是需要消耗 CPU 的,如果一直獲取不到鎖,那線程也不能一直占用 CPU 自旋做無用功,所以需要設定一個自旋等待的最大時間。JVM 對于自旋周期的選擇,JDK1.6 之后引入了適應性自旋鎖,適應性自旋鎖意味著自旋的時間不是固定的,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其他爭用鎖的線程在最大等待時間內還是獲取不到鎖,自旋不會一直持續下去,這時爭用線程會停止自旋進入阻塞狀態,該鎖膨脹為重量級鎖。4. 重量級鎖狀態
重量級鎖會讓其他申請的線程之間進入阻塞,性能降低。重量級鎖也就叫做同步鎖輕量級鎖的很總要一個實現基礎就是CAS操作(自旋):
CAS(Compare and swap),即比較并交換,也是實現我們平時所說的自旋鎖或樂觀鎖的核心操作。
- 執行函數:CAS(V,E,N) 其包含3個參數
- V 表示要更新的變量
- E 表示預期值
- N 表示新值
如何要更新的變量等于預期值,就把新值賦值給變量,如何要更新的變量不等于預期值,就CAS再重新試一下,再試的時候,會重新讀取要更新的變量作為預期值
比方說:
當前的這個線程想改這個值,我期望你是0,你就不能是1;如果是1,那就說明我這個值不對,然后想把你變成1。大概就是:原來這個值是變為3了,我這個線程想修改這個值的時候我一定期望你現在是3,是3我才改,如果在我修改的過程你變4了,說明就有另外一個線程修改過該值,那我cas就再重新試一下,再試的時候,我希望你的這個值是4,在修改的時候期望值是4,沒有其它線程修改該值,那好我給你改成5,這樣就是cas操作。
CAS 實現自旋鎖
既然用鎖或 synchronized 關鍵字可以實現原子操作,那么為什么還要用 CAS 呢,因為加鎖或使用 synchronized 關鍵字帶來的性能損耗較大,而用 CAS 可以實現樂觀鎖,它實際上是直接利用了 CPU 層面的指令,所以性能很高。
上面也說了,CAS 是實現自旋鎖的基礎,cas是cpu的原語支持,也就是說cas是cpu指令級別上的支持,中間不能被打斷,不會造成所謂的數據不一致問題,以達到鎖的效果,至于自旋呢,看字面意思也很明白,自己旋轉,翻譯成人話就是循環,一般是用一個無限循環實現。這樣一來,一個無限循環中,執行一個 CAS 操作,當操作成功,返回 true 時,循環結束;當返回 false 時,接著執行循環,繼續嘗試 CAS 操作,直到返回 true。
其實 JDK 中有好多地方用到了 CAS ,尤其是?java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如?java.util.concurrent.atomic?包下,相信大家都用到過 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
這里拿 AtomicBoolean 來舉個例子,因為它足夠簡單。
public class AtomicBoolean implements java.io.Serializable {private static final long serialVersionUID = 4654671469794556979L;// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(AtomicBoolean.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;public final boolean get() {return value != 0;}public final boolean compareAndSet(boolean expect, boolean update) {int e = expect ? 1 : 0;int u = update ? 1 : 0;return unsafe.compareAndSwapInt(this, valueOffset, e, u); //這里cas,會觸發cpu指令}}ABA問題
CAS 存在一個問題,就是一個值從 A 變為 B ,又從 B 變回了 A,這種情況下,CAS 會認為值沒有發生過變化,但實際上是有變化的。對此,并發包下倒是有 AtomicStampedReference 提供了根據版本號判斷的實現,可以解決一部分問題。
參考文章:我們常說的 CAS 自旋鎖是什么 - 風的姿態 - 博客園
總結
以上是生活随笔為你收集整理的【JUC系列】Java的锁机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【架构零】大型网站的架构的目标与挑战
- 下一篇: 【JUC系列】Future异步回调模式