1. 程式人生 > >Java中的偏向鎖,輕量級鎖, 重量級鎖解析

Java中的偏向鎖,輕量級鎖, 重量級鎖解析

參考文章

Java 中的鎖

在 Java 中主要2種加鎖機制:

  • synchronized 關鍵字
  • java.util.concurrent.LockLock是一個介面,ReentrantLock是該介面一個很常用的實現)

這兩種機制的底層原理存在一定的差別

  • synchronized 關鍵字通過一對位元組碼指令 monitorenter/monitorexit 實現, 這對指令被 JVM 規範所描述。
  • java.util.concurrent.Lock 通過 Java 程式碼搭配sun.misc.Unsafe 中的本地呼叫實現的

一些先修知識

先修知識 1: Java 物件頭

  • 字寬(Word): 記憶體大小的單位概念, 對於 32 位處理器 1 Word = 4 Bytes, 64 位處理器 1 Word = 8 Bytes
  • 每一個 Java 物件都至少佔用 2 個字寬的記憶體(陣列型別佔用3個位元組的字寬)。
    • 第一個字寬也被稱為物件頭Mark Word。 物件頭包含了多種不同的資訊, 其中就包含物件鎖相關的資訊。
    • 第二個字寬是指向定義該物件類資訊(class metadata)的指標
  • 非陣列型別的物件頭的結構如下圖
    這裡寫圖片描述

先修知識 2: CAS 指令

  • CAS (Compare And Swap) 指令是一個CPU層級的原子性操作指令。 在 Intel 處理器中, 其彙編指令為 cmpxchg。
  • 該指令概念上存在 3 個引數, 第一個引數【目標地址】, 第二個引數【值1】, 第三個引數【值2】, 指令會比較【目標地址儲存的內容】和 【值1】 是否一致, 如果一致, 則將【值 2】 填寫到【目標地址】, 其語義可以用如下的偽程式碼表示。
function cas(p , old , new ) returns bool {
    if *p ≠ old { // *p 表示指標p所指向的記憶體地址
        return false
    }
    *p ← new
    return true
}
  • 注意: 該指令是是原子性的, 也就是說 CPU 執行該指令時, 是不會被中斷執行其他指令的

先修知識 3: “CAS”實現的"無鎖"演算法常見誤區

  • 誤區一: 通過簡單應用 “比較後再賦值” 的操作即可輕鬆實現很多無鎖演算法
    • CAS 指令的一個不可忽略的特徵是原子性。 在 CPU 層面, CAS 指令的執行是有原子性語義保證的, 如果 CAS 操作放在應用層面來實現, 則需要我們自行保證其原子性。 否則就會發生如下描述的問題:
// 下列的函式如果不是執行緒互斥的
function cas( p , old , new) returns bool {
    if *p ≠ old { // 此處的比較操作進行時, 可以同時有多個執行緒通過該判斷
        return false
    }
    *p ← new // 多個執行緒的賦值操作會相互覆蓋, 造成程式邏輯的錯誤
    return true
}
  • 誤區二: CAS 操作的 ABA 問題
    • 大部分網路博文對 ABA 問題的常見描述是: 應用 CAS 操作時, 目標地址的值剛開始為 A, 工作執行緒/程序 讀取後, 進行了一系列運算, 計算得出了新值 C, 在此期間, 目標地址的值被其他執行緒已經進行了不止一次修改, 其值已經從 A 被改為 B , 又改回 A, 此時便會發生同步問題。
    • 上面的描述是其實是錯誤的, 思考一下就會發現, 如果工作執行緒的操作目的是將目標地址的值從 A 改為 C, 那麼即便在這期間目標地址的值經過了其他執行緒或程序的多次修改, 其語義依舊是正確的。
    • 例如目前要將某銀行賬號的餘額扣除 50, 通過 CAS 保證同步 :
      • 首先讀取原有餘額為 100 ,
      • 計算餘額應該賦值為 100 - 50 = 50
      • 此時該執行緒被掛起, 該賬戶同時又發生了轉入 150 和轉出 150 的操作, 餘額經歷了 100 -》250 -》100 的變動
      • 執行緒被喚醒, 進行 CAS 賦值操作 cas(p, 100, 50) , 正常得以執行。
      • 該賬戶的餘額依舊是正確的
    • 通過上述例子就可以發現, ABA 的問題並不在於多次修改。 查閱一下 CAS 的 wiki 解釋, 就會發現, ABA 真正的問題是, 假如目標地址的內容被多次修改以後, 雖然從二進位制上來看是依舊是 A, 但是其語義已經不是 A 。例如, 發生了整數溢位, 記憶體回收等等。

先修知識 4: 棧幀(Stack Frame) 的概念

  • 這個概念涉及的內容較多, 不便於展開敘述。 從理解下文的角度上來講, 需要知道, 每個執行緒都有自己獨立的記憶體空間, 棧幀就是其中的一部分。裡面可以儲存僅屬於該執行緒的一些資訊。
  • 需要深入瞭解的同學, 需要自行查閱 棧幀 相關的概念

synchronized 關鍵字之鎖的升級(偏向鎖->輕量級鎖->重量級鎖)

前面提到過, synchronized 程式碼塊是由一對 monitorenter/moniterexit 位元組碼指令實現, monitor 是其同步實現的基礎, Java SE1.6 為了改善效能, 使得 JVM 會根據競爭情況, 使用如下 3 種不同的鎖機制

  • 偏向鎖(Biased Lock )
  • 輕量級鎖( Lightweight Lock)
  • 重量級鎖(Heavyweight Lock)

上述這三種機制的切換是根據競爭激烈程度進行的, 在幾乎無競爭的條件下, 會使用偏向鎖, 在輕度競爭的條件下, 會由偏向鎖升級為輕量級鎖, 在重度競爭的情況下, 會升級到重量級鎖。

注意 JVM 提供了關閉偏向鎖的機制, JVM 啟動命令指定如下引數即可

-XX:-UseBiasedLocking

下圖展現了一個物件在建立(allocate) 後, 根據偏斜鎖機制是否開啟, 物件 MarkWord 狀態以不同方式轉換的過程

這裡寫圖片描述

無鎖 -> 偏向鎖

從上圖可以看到 , 偏向鎖的獲取方式是將物件頭的 MarkWord 部分中, 標記上執行緒ID, 以表示哪一個執行緒獲得了偏向鎖。 具體的賦值邏輯如下:

  • 首先讀取目標物件的 MarkWord, 判斷是否處於可偏向的狀態(如下圖)
    這裡寫圖片描述

    • (實際 JDK 程式碼中,其實是通過標誌位結合 epoch 值去判斷是否處於可偏向的狀態, 而不是根據 ThreadId 為 0 來判斷可以嘗試偏向鎖獲取的, 這一點從文章後續的原始碼解析中看到)
  • 如果為可偏向狀態, 則嘗試用 CAS 操作, 將自己的執行緒 ID 寫入MarkWord

    • 如果 CAS 操作成功(狀態轉變為下圖), 則認為已經獲取到該物件的偏向鎖, 執行同步塊程式碼
      這裡寫圖片描述
    • 如果 CAS 操作失敗, 則說明, 有另外一個執行緒搶先獲取了偏向鎖, 此時需要撤銷偏向鎖,使目標物件進入輕量級鎖的狀態。 該操作需要等待全域性安全點 JVM safepoint ( 此時間點, 沒有執行緒在執行位元組碼) 。
  • 如果是已偏向狀態, 則檢測 MarkWord 中儲存的 thread ID 是否等於當前 thread ID 。

    • 如果相等, 則證明本執行緒已經獲取到偏向鎖, 可以直接繼續執行同步程式碼塊
    • 如果不等, 則證明該物件目前偏向於其他執行緒, 需要撤銷偏向鎖

從上面的偏向鎖機制描述中,可以注意到

  • 偏向鎖的 撤銷(revoke) 是一個很特殊的操作, 為了執行撤銷操作, 需要等待全域性安全點(Safe Point), 此時間點所有的工作執行緒都停止了位元組碼的執行。
  • 一個執行緒在執行完同步程式碼塊以後, 並不會嘗試將 MarkWord 中的 thread ID 賦回原值 。如果該執行緒需要再次加鎖時, 會發現之前已經獲得偏向鎖, 無須修改物件頭的任何內容, 最小化開銷。

偏向鎖的批量再偏向(Bulk Rebias)機制

偏向鎖這個機制很特殊, 別的鎖在執行完同步程式碼塊後, 都會有釋放鎖的操作, 而偏向鎖並沒有直觀意義上的“釋放鎖”操作。

那麼作為開發人員, 很自然會產生的一個問題就是, 如果一個物件先偏向於某個執行緒, 執行完同步程式碼後, 另一個執行緒就不能直接重新獲得偏向鎖嗎? 答案是可以, JVM 提供了批量再偏向機制(Bulk Rebias)機制

該機制的主要工作原理如下:

  • 引入一個概念 epoch, 其本質是一個時間戳 , 代表了偏向鎖的有效性
  • 從前文描述的物件頭結構中可以看到, epoch 儲存在可偏向物件的 MarkWord 中。
  • 除了物件中的 epoch, 物件所屬的類 class 資訊中, 也會儲存一個 epoch 值
  • 每當遇到一個全域性安全點時, 如果要對 class C 進行批量再偏向, 則首先對 class C 中儲存的 epoch 進行增加操作, 得到一個新的 epoch_new
  • 然後掃描所有持有 class C 例項的執行緒棧, 根據執行緒棧的資訊判斷出該執行緒是否鎖定了該物件, 僅將 epoch_new 的值賦給被鎖定的物件中。
  • 退出安全點後, 當有執行緒需要嘗試獲取偏向鎖時, 直接檢查 class C 中儲存的 epoch 值是否與目標物件中儲存的 epoch 值相等, 如果不相等, 則說明該物件的偏向鎖已經失效了, 可以直接通過 CAS 操作嘗試再次將該物件再次偏向於請求獲得鎖的執行緒。

上述的邏輯可以在 JDK 原始碼中得到驗證。

在 sharedRuntime.cpp 中, 下面程式碼是 synchronized 的主要邏輯

Handle h_obj(THREAD, obj);
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, lock, CHECK);
  }
  • UseBiasedLocking 是 JVM 啟動時, 偏斜鎖是否啟用的標誌。
  • fast_enter 中包含了偏斜鎖的相關邏輯
  • slow_enter 中繞過偏斜鎖, 直接進入輕量級鎖獲取
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
                                    bool attempt_rebias, TRAPS) {
  if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }

  slow_enter(obj, lock, THREAD);
}

  • 該函式中再次保險性地做了偏斜鎖是否開啟的檢查(UseBiasedLocking)
  • 當系統不處於安全點時, 程式碼通過方法 revoke_and_rebias 這個函式嘗試獲取偏斜鎖, 如果獲取成功就可以直接返回了, 如果不成功則進入輕量級鎖的獲取過程
  • revoke_and_rebias 這個函式名稱就很有意思, 說明該函式中包含了 revoke 的操作也包含了 rebias 的操作
    • revoke 不是隻應該在安全點時刻才發生嗎? 答案: 有一些特殊情形, 不需要安全點也可以執行 revoke 操作
    • 此處為什麼只有 rebias 操作, 沒有初次的 bias 操作?答案: 首次的 bias 操作也被當成了 rebias 操作的一個特例
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
  assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");

  // We can revoke the biases of anonymously-biased objects
  // efficiently enough that we should not cause these revocations to
  // update the heuristics because doing so may cause unwanted bulk
  // revocations (which are expensive) to occur.
  markOop mark = obj->mark();
  if (mark->is_biased_anonymously() && !attempt_rebias) {
      /* 
		    進一步檢視原始碼可得知, is_biased_anonymously() 為 true 的條件是物件處於可偏向狀態, 
		    且 執行緒ID  為空, 表示尚未偏向於任意一個執行緒。 
		    此分支是進行物件的 hashCode 計算時會進入的, 根據 markWord 結構可以看到, 
		    當一個物件處於可偏向狀態時, markWord 中 hashCode 的儲存空間是被佔用的
		    所以需要 revoke 可偏向狀態, 以提供儲存 hashCode 的空間
		 */
    
    // We are probably trying to revoke the bias of this object due to
    // an identity hash code computation. Try to revoke the bias
    // without a safepoint. This is possible if we can successfully
    // compare-and-exchange an unbiased header into the mark word of
    // the object, meaning that no other thread has raced to acquire
    // the bias of the object.
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
    markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
    if (res_mark == biased_value) {
      return BIAS_REVOKED;
    }
  } else if (mark->has_bias_pattern()) {
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (!prototype_header->has_bias_pattern()) {
      // This object has a stale bias from before the bulk revocation
      // for this data type occurred. It's pointless to update the
      // heuristics at this point so simply update the header with a
      // CAS. If we fail this race, the object's bias has been revoked
      // by another thread so we simply return and let the caller deal
      // with it.
      markOop biased_value       = mark;
      markOop res_mark = obj->cas_set_mark(prototype_header, mark);
      assert(!obj->mark()->has_bias_pattern(), "even if we raced, should still be revoked");
      return BIAS_REVOKED;
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { 
	    /* 
		    這個分支就是首次獲取偏向鎖會進入的分支
			筆者根據此處的程式碼推斷, 一個物件在剛剛分配出來的時候, 
			其物件頭中儲存的 epoch 值和 class 中儲存的 epoch 值是不一樣的,
			以體現物件從未被偏向過
		 */
    
      // The epoch of this biasing has expired indicating that the
      // object is effectively unbiased. Depending on whether we need
      // to rebias or revoke the bias of this object we can do it
      // efficiently enough with a CAS that we shouldn't update the
      // heuristics. This is normally done in the assembly code but we
      // can reach this point due to various points in the runtime
      // needing to revoke biases.
      if (attempt_rebias) {
	    /*
			下面的程式碼就是嘗試通過 CAS 操作, 將本執行緒的 ThreadID 嘗試寫入物件頭中
		*/
        assert(THREAD->is_Java_thread(), "");
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = obj->cas_set_mark(rebiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED;
        }
      } else {
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
        markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;
        }
      }
    }
  }

偏向鎖 -> 輕量級鎖

從之前的描述中可以看到, 存在超過一個執行緒競爭某一個物件時, 會發生偏向鎖的撤銷操作。 有趣的是, 偏向鎖撤銷後, 物件可能處於兩種狀態。

  • 一種是不可偏向的無鎖狀態, 如下圖(之所以不允許偏向, 是因為已經檢測到了多於一個執行緒的競爭, 升級到了輕量級鎖的機制)
    這裡寫圖片描述

  • 另一種是不可偏向的已鎖 ( 輕量級鎖) 狀態
    這裡寫圖片描述

之所以會出現上述兩種狀態, 是因為偏向鎖不存在解鎖的操作, 只有撤銷操作。 觸發撤銷操作時, 物件既有可能處於“被佔用狀態”, 也有可能處於 “閒置狀態”, 如果是被佔用狀態,則物件就應該被轉換為已加鎖狀態。

輕量級加鎖過程:

  • 首先根據標誌位判斷出物件狀態處於不可偏向的無鎖狀態( 如下圖)
    這裡寫圖片描述
  • 在當前執行緒的棧楨(Stack Frame)中建立用於儲存鎖記錄(lock record)的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。如果在此過程中發現,
  • 然後執行緒嘗試使用 CAS 操作將物件頭中的 Mark Word 替換為指向鎖記錄的指標。
    • 如果成功,當前執行緒獲得鎖
    • 如果失敗,表示該物件已經被加鎖了, 先進行自旋操作, 再次嘗試 CAS 爭搶, 如果仍未爭搶到, 則進一步升級鎖至重量級鎖。

重量級鎖

重量級鎖依賴於作業系統的互斥量(mutex) 實現, 其具體的詳細機制此處暫不展開, 日後可能補充。 此處暫時只需要瞭解該操作會導致程序從使用者態與核心態之間的切換, 是一個開銷較大的操作。

存疑的問題

  1. 在鎖膨脹的圖例中, 執行緒 2 線上程 1 尚未釋放鎖時, 即將物件頭修改為指向重量級鎖的狀態, 這個操作具體如何完成, 是否需要等待全域性安全點?筆者尚未細究

  2. biasedLocking.cpp中的方法 revoke_and_rebias 存在 4 個條件分支, 其中筆者添加了註釋的兩個分支其主要邏輯已經清晰, 但未添加註釋的兩個賦值具體邏輯筆者尚不清楚, 有待進一步研究

相關推薦

java學習筆記——java對象的創建初始化引用的解析

初始 學習筆記 style article 學習 base 表達 如果 bsp 如果有一個A類。 1、例如以下表達式: A a1 = new A(); 那麽A是類,a1是引用。new A()是對象。僅僅是a1這個引用指向了new A()這個對象。 2、又如: A

java參數傳遞--值傳遞引用傳遞

新的 結果 html 參數傳遞 參數 一個 程序員 java中的對象 傳參 java中的參數傳遞——值傳遞、引用傳遞 參數是按值而不是按引用傳遞的說明 Java 應用程序有且僅有的一種參數傳遞機制,即按值傳遞。 在 Java 應用程序中永遠不會傳遞對象,而只傳遞對象

JAVA操作CLOB大對象 提示ORA-01704字符串文字太長

CLOB ORACEL java 分析:在ORACEL中大文本的不能直接插入,是因為oracle會將clob自動轉為String,當文本字節超出4000字節,提示字符太長。備註: GBK編碼:一個漢字占兩個字節。 UTF-16編碼:通常漢字占兩個字節,CJKV擴展B區、擴展C區、擴展D區中的漢字占

關於Java反射問題的權威解答歡迎查閱!

1.反射介紹 在執行階段,動態獲取類的資訊和呼叫類的屬性和方法的機制稱為反射機制 2.反射的作用 獲取物件所屬的類(父類,介面) 通過類建立物件 獲取物件所有的屬性和方法(呼叫) 建立代理物件 3.反射採用api(java.lang.reflec

java輸入三個字元後按各字元的ASCII碼從小到大的順序輸出這三個字元。

import java.util.Scanner; public class Main {     public static void main(String[] args) {         Scanne

優先順序PK:Java的4種程式碼塊誰先誰後?

問題:Java裡的四種程式碼塊,像積木一般搭成程式碼塔。那麼一段複雜的程式碼在JVM裡每一句的執行順序是如何的呢? 思路:看程式碼塊的優先順序順序——>被呼叫的順序 答:要回答這個問題,我們先來看看四種程式碼塊是哪些: 有了定義,還是要具體例子的: public cl

Java使用Set進行並集差集交集查詢

利用Java中使用Set進行並集,差集,交集查詢 首先命名一個類名為DealSet存放查詢並集,差集,交集的方法 DealSet.java package SetLearning; import java.util.HashSet; import java.util.Set; p

JavaFinal修飾一個變數時是引用不能變還是引用的物件不能變

Java中,使用Final修飾一個變數,是引用不能變,還是引用物件不能變?   是引用物件的地址不能變,引用變數所指的物件的內容可以改變。   final變數永遠指向這個物件,是一個常量指標,而不是指向常量的指標。   比如: final StringBuffer sb=new Stri

圖解 Java 的資料結構及原理傻瓜也能看懂!

最近在整理資料結構方面的知識, 系統化看了下Java中常用資料結構, 突發奇想用動畫來繪製資料流轉過程。 主要基於jdk8, 可能會有些特性與jdk7之前不相同, 例如LinkedList LinkedHashMap中的雙向列表不再是迴環的。 HashMap中的單鏈表是尾插, 而不是頭插入等等, 後文

JavahasNext()與next()的區別hasNextInt()與nextInt()的區別hasNextDouble()與nextDouble()的區別

轉載自:https://blog.csdn.net/weixin_37770552/article/details/77431961 還有補充:https://zhidao.baidu.com/question/198579166802848525.html java.util.Scanne

java呼叫Oracle儲存過程時出現異常:java.sql.SQLException: ORA-00928: 缺失 SELECT 關鍵字(已解決)

在java中呼叫Oracle儲存過程時,出現異常:java.sql.SQLException: ORA-00928: 缺失 SELECT 關鍵字 //java程式碼 @Test public void testProcedure(){

java的excel表格的匯出實際專案開發

java中匯出excel格式的資料,實際專案 經歷 首先 先整出一個表格模板,這是一個標準的格式: public class ExcelExport { private static final String[] PART_METASIGNIN = {“姓名”,

java使用mybatis呼叫儲存過程拿到返回值(單引數返回值)

service業務層呼叫dao層 注意:返回值直接從物件裡獲取 不需要拿物件接收再獲取 dao.uspGetUser(userPO);//物件封裝了儲存過程的入參和出參 count = userPO.getCount(); //count 是儲存過程的返回值 dao層介面 pu

Java的內部類詳解為什麼需要內部類?

內部類的共性   內部類分為: 成員內部類、靜態巢狀類、方法內部類、匿名內部類。      (1)、內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class檔案,但是前面冠以外部類的類

java內部類在區域性時訪問許可權》

/* 內部類定義在區域性時, 1,不可以被成員修飾符修飾 2,可以直接訪問外部類中的成員,因為還持有外部類中的引用。 但是不可以訪問它所在的區域性中的變數。只能訪問被final修飾的區域性變數。 *

JavaJMX管理器的作用專案有什麼具體使用?

作者:wuxinliulei 連結:https://www.zhihu.com/question/36688387/answer/68667704 來源:知乎 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。   JMX是一種JAVA的正式規範,它主要目的是讓程式有

Java多執行緒程式設計-(12)-Java的佇列同步器AQS和ReentrantLock原理簡要分析

原文出自 : https://blog.csdn.net/xlgen157387/article/details/78341626 一、Lock介面 在上一篇文章中: Java多執行緒程式設計-(5)-使用Lock物件實現同步以及執行緒間通訊 介紹

java執行多條shell命令除了第一條其他都未執行

最近專案中需要在在java中執行shell命令,用了最常見方式,程式碼如下: public class ShellUtil { public static String runShell(String shStr) throws Exception

Java會存在記憶體洩漏嗎請簡單描述。

記憶體洩漏是指不再被使用的物件或者變數一直被佔據在記憶體中。 理論上來說,Java是有GC垃圾回收機制的,也就是說,不再被使用的物件,會被GC自動回收掉,自動從記憶體中清除。 但是,即使這樣,Java也還是存在著記憶體洩漏的情況, 1、長生命週期的物件持有短生命週期物件的引用就很可能

java利用jsch執行遠端命令實現sftp

利用jsch可以執行遠端命令並實現sftp檔案傳輸,以下為自定義的util: import com.jcraft.jsch.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; impor