Java 併發程式設計(一) → LockSupport 詳解

青石路發表於2021-05-17

開心一刻

  今天突然收到花唄推送的訊息,說下個月 9 號需要還款多少錢

  我就納了悶了,我很長時間沒用花唄了,怎麼會欠花唄錢?

  後面我一想,兒子這幾天玩了我手機,是不是他偷摸用了我的花唄

  於是我找到兒子問了起來

  我:兒子,你是不是用了我的花唄

  兒子:是的呀,爸,我就用了一點

  我:額度就剩兩塊了,你用了我用什麼?

  兒子:你用你爸的唄!

  我:...

  不對呀,我女朋友都沒有,哪裡的兒子?猛的被驚醒,大白天的,我特麼竟然還做上了白日夢!

Java 併發程式設計(一) → LockSupport 詳解

前言

  本文是基於 JDK1.8

  那麼此時 Java 執行緒與作業系統執行緒的對應關係是 1:1 的,有興趣的可以讀一讀:深入聊聊java執行緒模型實現?

  至於 Java 是否在未來引入類似 Go 中的協程,從而實現 Java 執行緒與作業系統執行緒的關係是 m:n,那是未來的事,那就未來再說

  我們能確定的是:Java8 中,Java 執行緒與作業系統執行緒是 1:1 的

LockSupport 簡介

  關於 LockSupport,我們對它感到很陌生,因為我們在工作中很少直接接觸到它,但多多少少,我們都間接用到過它

  LockSupport 是 JUC 包下很重要的一個工具類,我們來看看它的原始碼概述:

    Basic thread blocking primitives for creating locks and other synchronization classes

    用於建立鎖和其他同步類的基本執行緒阻塞原語

  JUC 包下的鎖、同步類基本都依賴 LockSupport 實現執行緒的阻塞與喚醒

  我們可以簡單的認為 LockSupport 對 Java 執行緒(作業系統執行緒)的阻塞與喚醒進行了封裝,簡化了開發人員的任務

  permit(許可證)

  LockSupport 的設計思路就是為每一個執行緒設定一個 permit,其實就是一個值,類似於 AQS 中的 state

  但 permit 沒有顯示的存在於 LockSupport 的原始碼中,而 state 卻顯示的存在於 AQS 的原始碼中( private volatile int state; )

    permit 預設值(初始值)是 0,permit 最小值是 0,最大值是 1;0 表示許可證不可用,1 表示許可證可用

    若 permit 值為 0,則 park 方法會阻塞當前執行緒,直至超時或有可用的 permit;若 permit 為 1 ,則 park 方法會將 permit 值設定成 0,不會阻塞當前執行緒

    不管 permit 的值是 0 還是 1,unpark 方法會將 permit 設定成 1,也就說多次 unpark (中間沒有 park)後,permit 的值仍是 1

  那麼問題來了,permit 不在 LockSupport 中,那麼它在哪?

  其實 permit 體現在 JVM 中,我們來看看在 Hotspot 中對應的原始碼,在 /hotspot/src/share/vm/runtime/park.hpp 中有如下一段

Java 併發程式設計(一) → LockSupport 詳解
class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  Parker * FreeNext ;
  JavaThread * AssociatedWith ; // Current association

public:
  Parker() : PlatformParker() {
    _counter       = 0 ;
    FreeNext       = NULL ;
    AssociatedWith = NULL ;
  }
protected:
  ~Parker() { ShouldNotReachHere(); }
public:
  // For simplicity of interface with Java, all forms of park (indefinite,
  // relative, and absolute) are multiplexed into one call.
  void park(bool isAbsolute, jlong time);
  void unpark();

  // Lifecycle operators
  static Parker * Allocate (JavaThread * t) ;
  static void Release (Parker * e) ;
private:
  static Parker * volatile FreeList ;
  static volatile int ListLock ;

};
View Code

  這個 volatile int _counter 就是 permit 的底層具體實現

LockSupport 核心方法

  方法不多,如下圖

  

  主要分兩類:park 和 unpark ,我們針對這幾個方法,一個一個來看,注意多看註釋

  park

  會消耗 permit,若當前沒有可用的 permit,則會阻塞當前執行緒

  park()

    方法體非常簡單

    簡單的一行: UNSAFE.park(false, 0L); 關於 Unsafe,有興趣的可以去了解下:Java魔法類:Unsafe應用解析

    只看這個程式碼,我們很難看出什麼,所幸有方法註釋,簡單翻譯一下

    1、除非 permit 可用,否則阻塞當前執行緒直至 permit 可用

    2、如果 permit 可用,會將 permit 設定成 0,立即返回,不會阻塞當前執行緒

    3、當 permit 不可用時,當前執行緒會被阻塞,直至發生以下三種情況

      3.1 其他執行緒呼叫 unpark 喚醒此執行緒

      3.2 其他執行緒通過 Thread#interrupt 中斷此執行緒

      3.3 該呼叫不合邏輯地(即毫無理由地)返回,可能是作業系統異常導致的

    4、park() 不會報告是什麼原因導致的呼叫返回,有需要的話,呼叫者需在返回時自行檢查是什麼條件導致呼叫返回

  park(Object blocker)

    方法體也很簡單

    功能與 park() 一樣,只是多了個入參:Object blocker ,線上程被阻止時記錄此物件,以允許監視和診斷工具識別執行緒被阻止的原因

    我們通過 jstack 命令,來看看 park() 和 park(Object blocker) 執行緒快照資訊有什麼區別

    示例程式碼:

    用 park() 時執行緒 t1 的快照資訊如下

    用 park(Object blocker) 時執行緒 t1 的快照資訊如下

    我們發現 park(Object blocker) 多了一行: - parking to wait for <0x000000076bbb5108> (a java.lang.String) 

    當然 park(Object blocker) 不會像示例中那麼使用(傳個固定的字串),傳的肯定是有意義的物件,我們來看看 JDK 中哪些地方用到了它

    感興趣的可以去看看具體的程式碼,其中的 this 具體是什麼,它作為 blocker 有什麼作用

  parkNanos(long nanos)

    nanos 表示等待的最大納秒數;我們來翻譯一下方法的註釋

    1、除非 permit 可用,否則阻塞當前執行緒直至 permit 可用,或者等待的時間結束

    2、如果 permit 可用,會將 permit 設定成 0,立即返回,不會阻塞當前執行緒

    3、當 permit 不可用時,當前執行緒會被阻塞,直至發生以下四種情況

      3.1 其他執行緒呼叫 unpark 喚醒此執行緒

      3.2 其他執行緒通過 Thread#interrupt 中斷此執行緒

      3.3 經過指定的等待時間,不會無限期的等待下去

      3.4 該呼叫不合邏輯地(即毫無理由地)返回,可能是作業系統異常導致的

    4、park() 不會報告是什麼原因導致的呼叫返回,有需要的話,呼叫者需在返回時自行檢查是什麼條件導致呼叫返回

    可以看出,功能與 park() 基本一致,只是多了一個等待時長

  parkNanos(Object blocker, long nanos)

    功能與 parkNanos(long nanos) 基本一樣,只是多了個 Object blocker 

    將 parkNanos(Object blocker, long nanos) 與 parkNanos(long nanos)  的關係與 park(Object blocker) 於 park() 的關係進行類比,就好理解了

    JDK 中有很多地方用到了 parkNanos(Object blocker, long nanos)

  parkUntil(long deadline)

    dealine 表示等待到的絕對時間,以毫秒為單位

    功能與 parkNanos(long nanos) 基本一致,只是 parkNanos(long nanos) 等待的是相對時長(納秒),而 parkUntil(long deadline) 等待的則是絕對時間點(毫秒)

  parkUntil(Object blocker, long deadline)

    功能與 parkUntil(long deadline),只是多了個 Object blocker

    將 parkUntil(Object blocker, long deadline) 與 parkUntil(long deadline) 的關係與 parkNanos(Object blocker, long nanos) 與 parkNanos(long nanos)  的關係進行列表,就好理解了

    JDK 中有些地方用到了 parkUntil(Object blocker, long deadline) 

  unpark

  方法體非常簡單

  我們來翻一下它的註釋

  1、使入參執行緒的 permit 可用(將 permit 設定成 1)

  2、如果入參執行緒正阻塞於 park,那麼會喚醒入參執行緒,否則入參執行緒的下一次 park 不會阻塞

  3、如果入參執行緒還沒有啟動,它不會產生任何效果

  4、如果入參執行緒為null,它不會產生任何效果

  JDK 中有很多地方用到了它

使用場景

  因為 JDK 已經提供了豐富的 API,所以我們平時基本不會直接使用 LockSupport,所以很多人認為 LockSupport 離我們很遠

  其實不然,只要我們用到 JUC 下的類來進行併發程式設計,那麼就已經間接用到了 LockSupport 了

  JUC 中執行緒的阻塞與喚醒的實現,依賴的都是 LockSupport

  執行緒交替列印

    這是樓主之前遇到的一個面試題,LockSupport 就是其中的一個考點,具體可檢視:記一個有意思的面試題 → 執行緒交替輸出問題

    用 LockSupport 是最優的解決方式,不依賴於第三方的同步值,程式碼簡單,邏輯清晰,非常好理解和實現

總結

  1、park 分三類,每類分兩種,官方推薦用帶 blocker 引數的那一種

    park()、park(Object blocker)

    parkNanos(long nanos)、parkNanos(Object blocker, long nanos)

    parkUntil(long deadline)、parkUntil(Object blocker, long deadline)

  2、park 與 unpark 之間沒有嚴格的呼叫先後順序

    permit = 1 表示可用,permit = 0 表示不可用;permit 屬於執行緒私有

    park 消耗 permit,將 permit 從 1 設定成 0;unpark 則將 permit 設定成 1,不管設定前的值是 1 還是 0

    permit 可用,則 park 不會阻塞當前執行緒,將 permit 設定成 0,執行緒繼續往下執行,否則 park 會阻塞當前執行緒

    unpark 會設定指定執行緒的 permit = 1,並喚醒指定的執行緒

參考

  Java魔法類:Unsafe應用解析

  JVM 常見線上問題 → CPU 100%、記憶體洩露 問題排查

相關文章