1. 程式人生 > 實用技巧 >Java中的鎖[原理、鎖優化、CAS、AQS]

Java中的鎖[原理、鎖優化、CAS、AQS]

參考地址: https://mp.weixin.qq.com/s/D44S7HtEZBSuDI1biAXbVw

1、為什麼要用鎖?

鎖-是為了解決併發操作引起的髒讀、資料不一致的問題。

2、鎖實現的基本原理

2.1、volatile

Java程式語言允許執行緒訪問共享變數, 為了確保共享變數能被準確和一致地更新,執行緒應該確保通過排他鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖要更加方便。

volatile在多處理器開發中保證了共享變數的“ 可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值。

image.png

結論:如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起執行緒上下文的切換和排程。

2.2、synchronized

synchronized通過鎖機制實現同步。

先來看下利用synchronized實現同步的基礎:Java中的每一個物件都可以作為鎖。

具體表現為以下3種形式。

  • 對於普通同步方法,鎖是當前例項物件。
  • 對於靜態同步方法,鎖是當前類的Class物件。
  • 對於同步方法塊,鎖是Synchonized括號裡配置的物件。

當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或丟擲異常時必須釋放鎖。

2.2.1 synchronized實現原理

synchronized是基於Monitor來實現同步的。

Monitor從兩個方面來支援執行緒之間的同步:

  • 互斥執行
  • 協作

1、Java 使用物件鎖 ( 使用 synchronized 獲得物件鎖 ) 保證工作在共享的資料集上的執行緒互斥執行。

2、使用 notify/notifyAll/wait 方法來協同不同執行緒之間的工作。

3、Class和Object都關聯了一個Monitor。

Monitor 的工作機理

  • 執行緒進入同步方法中。
  • 為了繼續執行臨界區程式碼,執行緒必須獲取 Monitor 鎖。如果獲取鎖成功,將成為該監視者物件的擁有者。任一時刻內,監視者物件只屬於一個活動執行緒(The Owner)
  • 擁有監視者物件的執行緒可以呼叫 wait() 進入等待集合(Wait Set),同時釋放監視鎖,進入等待狀態。
  • 其他執行緒呼叫 notify() / notifyAll() 介面喚醒等待集合中的執行緒,這些等待的執行緒需要重新獲取監視鎖後才能執行 wait() 之後的程式碼。
  • 同步方法執行完畢了,執行緒退出臨界區,並釋放監視鎖。

參考文件:https://www.ibm.com/developerworks/cn/java/j-lo-synchronized

2.2.2 synchronized具體實現

1、同步程式碼塊採用monitorenter、monitorexit指令顯式的實現。

2、同步方法則使用ACC_SYNCHRONIZED標記符隱式的實現。

通過例項來看看具體實現:

public class SynchronizedTest {

public synchronized void method1(){
System.out.println("Hello World!");
}

public void method2(){
synchronized (this){
System.out.println("Hello World!");
}
}
}

javap編譯後的位元組碼如下:

image.png

monitorenter

每一個物件都有一個monitor,一個monitor只能被一個執行緒擁有。當一個執行緒執行到monitorenter指令時會嘗試獲取相應物件的monitor,獲取規則如下:

  • 如果monitor的進入數為0,則該執行緒可以進入monitor,並將monitor進入數設定為1,該執行緒即為monitor的擁有者。
  • 如果當前執行緒已經擁有該monitor,只是重新進入,則進入monitor的進入數加1,所以synchronized關鍵字實現的鎖是可重入的鎖。
  • 如果monitor已被其他執行緒擁有,則當前執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor。

monitorexit

只有擁有相應物件的monitor的執行緒才能執行monitorexit指令。每執行一次該指令monitor進入數減1,當進入數為0時當前執行緒釋放monitor,此時其他阻塞的執行緒將可以嘗試獲取該monitor。

2.2.3 鎖存放的位置

鎖標記存放在Java物件頭的Mark Word中。

Java物件頭長度

32位JVM Mark Word 結構

32位JVM Mark Word 狀態變化

64位JVM Mark Word 結構

2.2.3 synchronized的鎖優化

JavaSE1.6為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”。

在JavaSE1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。

鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖:

無鎖競爭的情況下為了減少鎖競爭的資源開銷,引入偏向鎖。

image.png

輕量級鎖:

輕量級鎖所適應的場景是執行緒交替執行同步塊的情況。

image.png

**鎖粗化(Lock Coarsening):**也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴充套件成一個範圍更大的鎖。

**鎖消除(Lock Elimination):**鎖削除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行削除。

**適應性自旋(Adaptive Spinning):**自適應意味著自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個迴圈。

另一方面,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。

2.2.4 鎖的優缺點對比

image.png

2.3、CAS

CAS,在Java併發應用中通常指CompareAndSwap或CompareAndSet,即比較並交換。

1、CAS是一個原子操作,它比較一個記憶體位置的值並且只有相等時修改這個記憶體位置的值為新的值,保證了新的值總是基於最新的資訊計算的,如果有其他執行緒在這期間修改了這個值則CAS失敗。

CAS返回是否成功或者記憶體位置原來的值用於判斷是否CAS成功。

2、JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實現的。

優點:

  • 競爭不大的時候系統開銷小。

缺點:

  • 迴圈時間長開銷大。
  • ABA問題。
  • 只能保證一個共享變數的原子操作。

3、Java中的鎖實現

3.1、佇列同步器(AQS)

佇列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步元件的基礎框架。

3.1.1、它使用了一個int成員變量表示同步狀態。

image.png

3.1.2、通過內建的FIFO雙向佇列來完成獲取鎖執行緒的排隊工作。
  • 同步器包含兩個節點型別的應用,一個指向頭節點,一個指向尾節點,未獲取到鎖的執行緒會建立節點執行緒安全(compareAndSetTail)的加入佇列尾部。同步佇列遵循FIFO,首節點是獲取同步狀態成功的節點。

    image.png

  • 未獲取到鎖的執行緒將建立一個節點,設定到尾節點。如下圖所示:

image.png

  • 首節點的執行緒在釋放鎖時,將會喚醒後繼節點。而後繼節點將會在獲取鎖成功時將自己設定為首節點。如下圖所示:

    image.png

3.1.3、獨佔式/共享式鎖獲取

獨佔式:有且只有一個執行緒能獲取到鎖,如:ReentrantLock。

共享式:可以多個執行緒同時獲取到鎖,如:CountDownLatch

獨佔式
  • 每個節點自旋觀察自己的前一節點是不是Header節點,如果是,就去嘗試獲取鎖。

    image.png

  • 獨佔式鎖獲取流程:

image.png

共享式:
  • 共享式與獨佔式的區別:

    image.png

  • 共享鎖獲取流程:

image.png

4、鎖的使用用例

4.1、ConcurrentHashMap的實現原理及使用

ConcurrentHashMap類圖

ConcurrentHashMap資料結構

結論:ConcurrentHashMap使用的鎖分段技術。首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。