鎖優化--1

weixin_34249678發表於2018-04-01

1 自旋鎖

  • 痛點:由於執行緒的阻塞/喚醒需要CPU在使用者態和核心態間切換,頻繁的轉換對CPU負擔很重,進而對併發效能帶來很大的影響
  • 現象:通過大量分析發現,物件鎖的鎖狀態通常只會持續很短一段時間,沒必要頻繁地阻塞和喚醒執行緒
  • 原理:通過執行一段無意義的空迴圈讓執行緒等待一段時間,不會被立即掛起,看持有鎖的執行緒是否很快釋放鎖,如果鎖很快被釋放,那當前執行緒就有機會不用阻塞就能拿到鎖了,從而減少切換,提高效能
  • 隱患:若鎖能很快被釋放,那麼自旋效率就很好(真正執行的自旋次數越少效率越好,等待時間就少);但若是鎖被一直佔用,那自旋其實沒有做任何有意義的事但又白白佔用和浪費了CPU資源,反而造成資源浪費
    注意:自旋次數必須有個限度(或者說自旋時間),如果超過自旋次數(時間)還沒獲得鎖,就要被阻塞掛起
  • 使用: JDK1.6以上預設開啟-XX:+UseSpinning,自旋次數可通過-XX:PreBlockSpin調整,預設10次

2 自適應自旋鎖

  • 痛點:由於自旋鎖只能指定固定的自旋次數,但由於任務的差異,導致每次的最佳自旋次數有差異
  • 原理:通過引入"智慧學習"的概念,由前一次在同一個鎖上的自旋時間和鎖的持有者的狀態來決定自旋的次數,換句話說就是自旋的次數不是固定的,而是可以通過分析上次得出下次,更加智慧
  • 實現:若當前執行緒針對某鎖自旋成功,那下次自旋此時可能增加(因為JVM認為這次成功是下次成功的基礎),增加的話成功機率可能更大;反正,若自旋很少成功,那麼自旋次數會減少(減少空轉浪費)甚至直接省略自旋過程,直接阻塞(因為自旋完全沒有意義,還不如直接阻塞)
  • 補充:有了自適應自旋鎖,隨著程式執行和效能監控資訊的不斷完善,JVM對鎖的狀況預測會越來越準確,JVM會變得越來越智慧

3 阻塞鎖

3.1 阻塞鎖

  • 加鎖成功:當出現鎖競爭時,只有獲得鎖的執行緒能夠繼續執行
  • 加鎖失敗:競爭失敗的執行緒會由running狀態進入blocking狀態,並被放置到與目標鎖相關的一個等待佇列中
  • 解鎖:當持有鎖的執行緒退出臨界區,釋放鎖後,會將等待佇列中的一個阻塞執行緒喚醒,令其重新參與到鎖競爭中

3.2 公平鎖

  • 公平鎖就是獲得鎖的順序按照先到先得的原則,從實現上說,要求當一個執行緒競爭某個物件鎖時,只要這個鎖的等待佇列非空,就必須把這個執行緒阻塞並塞入隊尾(插入隊尾一般通過一個CAS操作保持插入過程中沒有鎖釋放)

3.3 非公平鎖

  • 相對的,非公平鎖場景下,每個執行緒都先要競爭鎖,在競爭失敗或當前已被加鎖的前提下才會被塞入等待佇列,在這種實現下,後到的執行緒有可能無需進入等待佇列直接競爭到鎖(隨機性)

4 鎖粗化

  • 痛點:多次連線在一起的加鎖、解鎖操作會造成
  • 原理:將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖
  • 使用:將多個彼此靠近的同步塊合同在一個同步塊 或 把多個同步方法合併為一個方法
  • 補充:在JDK內建的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操作,可合併
/**
  * StringBuffer是執行緒安全的字串處理類
  * 每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖
  */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}

5 鎖消除

  • 痛點:根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖
  • 原理: JVM在編譯時通過對執行上下文的描述,去除不可能存在共享資源競爭的鎖,通過這種方式消除無用鎖,即刪除不必要的加鎖操作,從而節省開銷
  • 使用: 逃逸分析和鎖消除分別可以使用引數-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(鎖消除必須在-server模式下)開啟
  • 補充:在JDK內建的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操作,可消除
/**
  * 比如執行10000次字串的拼接
  */
public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    for (int i = 0 ; i < 10000 ; i++){
        synchronizedDemo.append("kira","sally");
    }
}
public void append(String str1,String str2){
    //由於StringBuffer物件被封裝在方法內部,不可能存在共享資源競爭的情況
    //因此JVM會認為該加鎖是無意義的,會在編譯期就刪除相關的加鎖操作
    //還有一點特別要註明:明知道不會有執行緒安全問題,程式碼階段就應該使用StringBuilder
    //否則在沒有開啟鎖消除的情況下,StringBuffer不會被優化,效能可能只有StringBuilder的1/3
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str1).append(str2);
}

參考

併發番@Synchronized一文通(1.8版)

相關文章