Java 多線程學習(7)聊聊 LockSupport.park() 和 LockSupport.unpark()

轉載請註明原創出處,謝謝!

HappyFeet的博客

最近在忙着找工作、找房子,事兒也挺多的,加上又換了個城市,也就沒什麼心思寫博客了。如今工作已定,房子也租好了,是時候調整好自己的心態,開始寫博客了。

說實話,這段時間面了不少公司,和很多面試官交流了許多,感觸頗多,不過目前還沒想好怎麼寫,我會盡快將這段時間辭職及面試的體會整理成一篇博客發表出來,還請大家耐心等待!

暫且把面試的事擱下,咱們今天來聊 LockSupport.park() 和 LockSupport.unpark() 的底層原理。

爲什麼會聊到這兩個方法呢?原因是在閱讀 AQS 的源碼的時候發現這兩個方法調用的次數非常多!所以在繼續深入閱讀 AQS 源碼之前,先來熟悉一下 LockSupport.park() 和 LockSupport.unpark() 的底層實現,爲後續 AQS 的學習打下基礎。


一、LockSupport.park() 和 LockSupport.unpark()

1、那些你應該知道的基礎知識

park 翻譯成中文是 “停放” 的意思,在代碼中的該方法含義是 “掛起” 當前線程。

實際上,LockSupport 類中所提供的方法有許多,常用的有下面這幾個:

  • park():無限期掛起當前線程
  • parkNanos(long nanos):掛起當前線程一段時間
  • parkUntil(long deadline):在 deadline 之前一直掛起當前線程
  • unpark(Thread thread):喚醒 thread 線程

其中 unpark 用於喚醒線程,其他三個均用於掛起線程。

掛起線程又分爲無限期和有限期掛起,對應到線程的狀態是 WAITING(無限期等待)和 TIMED_WAITING(限期等待)。

一個被無限期掛起的線程恢復的方式有三種:

  • 其他線程調用了 unpark 方法,參數爲被掛起的線程

  • 其他線程中斷了被掛起的線程

  • The call spuriously (that is, for no reason) returns.

    這裏講的是虛假喚醒,可以參考以下幾篇資料:

    由於虛假喚醒的存在,在調用 park 時一般採用自旋的方式,僞代碼如下:

        while (!canProceed()) { 
            ...
            LockSupport.park(this);
        }
    

而有限期掛起的除了上面三種之外,還有第四種方式:

  • 經過了掛起的時間段或者是達到了指定的 deadline

2、它們有何特點?

This class associates, with each thread that uses it, a permit.

Java 文檔裏說到了,每個線程都關聯一個許可(permit)。

當許可可用時,調用 park 會立即返回,否則可能(虛假喚醒則不會被掛起)被掛起;如果許可不可用時,調用 unpark 會使得許可變成可用,而如果許可本身是可用時,調用 unpark 不會有任何影響。

可能直接看文字不是那麼的一目瞭然,我們來看幾個例子:

  • 例一:park 掛起線程,unpark 喚醒被掛起的線程
public static void exampleOne() {
    Thread thread = new Thread(() -> {
        while (flag) {

        }

        System.out.println("before first park");
        LockSupport.park();
        System.out.println("after first park");
        LockSupport.park();
        System.out.println("after second park");

    });

    thread.start();

    flag = false;

    sleep(20);

    System.out.println("before unpark.");
    LockSupport.unpark(thread);
}

輸出結果

before first park
before unpark
after first park

分析

首先,許可初始是不可用的;所以在調用 park 後 thread 被掛起,後續主線程調用了 unpark 方法喚醒了被掛起的 thread,輸出 after first park ,緊接着 thread 調用 park 繼續被掛起。

  • 例二:unpark 效果不會被累積
private static void exampleTwo() {
    Thread thread = new Thread(() -> {
        while (flag) {

        }

        System.out.println("before first park");
        LockSupport.park();
        System.out.println("after first park");
        LockSupport.park();
        System.out.println("after second park");

    });

    thread.start();

    LockSupport.unpark(thread);
    LockSupport.unpark(thread);

    flag = false;
}

輸出結果

before first park
after first park

分析

主線程先對 thread 執行兩次 unpark 操作,然後 thread 再連續調用兩次 park 方法,結果發現,第二個 park 會掛起 thread;這裏主要體現了 unpark 效果不會被累積,當許可可用時,調用 unpark 方法不會產生任何效果。

  • 例三:中斷對 park 的影響
private static void exampleThree() {
    Thread thread = new Thread(() -> {

        System.out.println("before first park");
        LockSupport.park();
        System.out.println("after first park");
        LockSupport.park();
        System.out.println("after second park");
        System.out.println("isInterrupted is " + Thread.interrupted());
        System.out.println("isInterrupted is " + Thread.interrupted());
        LockSupport.park();
        System.out.println("after third park");
    });

    thread.start();

    sleep(200);

    thread.interrupt();
}

輸出結果

before first park
after first park
after second park
isInterrupted is true
isInterrupted is false

分析

thread 先後共調用了三次 park,前兩次調用沒啥區別,在第三次調用之前調用了兩次 Thread.interrupted();從輸出結果來看,發現前兩次 park 並沒有生效,只有第三次 park 將線程掛起了,Why?

我們先來看 Thread.interrupted() 的作用:**判斷當前線程的中斷狀態,同時將中斷狀態清除。**實際上這裏只需要調用一次 Thread.interrupted() 即可,調用了兩次是爲了查看線程中斷狀態的變化。

當線程的中斷狀態爲 true 時,park 失去了效果,不會掛起線程;而當調用了 Thread.interrupted() 將中斷狀態清除之後,park 又恢復了效果。

所以這裏可以得出的結論:線程中斷會使 park 失效

完整示例代碼傳送門

二、源碼跟蹤

重頭戲來啦!上面講到了 park 和 unpark 的幾個方法,其實它們最終對應於 UNSAFE.park(boolean isAbsolute, long time) 和 UNSAFE.unpark(Object thread) 這兩個 native 方法。

下面我們就去看看這兩個 native 方法是如何實現的。

1、UNSAFE.park(boolean isAbsolute, long time)

(1)unsafe.cpp#Unsafe_Parkopenjdk/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time))
  UnsafeWrapper("Unsafe_Park");
  EventThreadPark event;
#ifndef USDT2
  HS_DTRACE_PROBE3(hotspot, thread__park__begin, thread->parker(), (int) isAbsolute, time);
#else /* USDT2 */
   HOTSPOT_THREAD_PARK_BEGIN(
                             (uintptr_t) thread->parker(), (int) isAbsolute, time);
#endif /* USDT2 */
  JavaThreadParkedState jtps(thread, time != 0);
  thread->parker()->park(isAbsolute != 0, time);
#ifndef USDT2
  HS_DTRACE_PROBE1(hotspot, thread__park__end, thread->parker());
#else /* USDT2 */
  HOTSPOT_THREAD_PARK_END(
                          (uintptr_t) thread->parker());
#endif /* USDT2 */
  ...
UNSAFE_END

點擊查看源碼

這裏有幾個分支判斷,不過可以看出無論是哪個分支,最終都會走 park(bool isAbsolute, jlong time) 這個方法。

(2)os_linux.cpp#Parker::parkopenjdk/hotspot/src/os/linux/vm/os_linux.cpp
void Parker::park(bool isAbsolute, jlong time) {
			...
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  JavaThread *jt = (JavaThread *)thread;

  		...
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  // Next, demultiplex/decode time arguments
  timespec absTime;
  if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
    return;
  }
  if (time > 0) {
    unpackTime(&absTime, isAbsolute, time);
  }
			...
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    return;
  }
  
  int status ;
  if (_counter > 0)  { // no wait needed
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
    // Paranoia to ensure our locked and lock-free paths interact
    // correctly with each other and Java-level accesses.
    OrderAccess::fence();
    return;
  }

  assert(_cur_index == -1, "invariant");
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
    if (status != 0 && WorkAroundNPTLTimedWaitHang) {
      pthread_cond_destroy (&_cond[_cur_index]) ;
      pthread_cond_init    (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
    }
  }

  		...
  _counter = 0 ;
  status = pthread_mutex_unlock(_mutex) ;
  OrderAccess::fence();

  // If externally suspended while waiting, re-suspend
  if (jt->handle_special_suspend_equivalent_condition()) {
    jt->java_suspend_self();
  }
}

點擊查看源碼

  • 首先通過 Atomic::xchg(0, &_counter) 方法將 _counter 置 0,如果原來的 _counter > 0,說明原來的許可是可用的,直接返回;

  • 如果當前線程的處於中斷狀態,直接返回;

Thread::is_interrupted(thread, false) 只會判斷線程的中斷狀態,不會重置其中斷狀態;Thread.interrupted() 調用的是 Thread::is_interrupted(thread, true),判斷線程的中斷狀態同時將其重置。

  • 如果 time < 0 或者 isAbsolute 爲 true 且 time = 0,說明不需要掛起線程,直接返回;否則將 time 換算成 absTime,後續有限期等待會用到。
  • 再次判斷 _counter 的值,如果大於 0,說明原來的許可是可用的,將其置 0,然後直接返回。這裏應該是在Atomic::xchg 到獲取鎖的之間有 unpark 操作,使得 _counter 的值變爲 1 了;
  • 判斷如果 time == 0,調用 pthread_cond_wait 方法,進入無限期等待;否則,調用 os::Linux::safe_cond_timedwait 方法,這個方法最終調用的是 pthread_cond_timedwait,從而進入有限期等待。

關於 pthread_cond_wait 可以看一下這篇文章: pthread_cond_wait 詳解

感覺和 Object.wait、Object.notify、Object.notifyAll 的機制很類似。

2、UNSAFE.unpark(Object thread)

(1)unsafe.cpp#Unsafe_Unparkopenjdk/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread))
  UnsafeWrapper("Unsafe_Unpark");
  Parker* p = NULL;
  if (jthread != NULL) {
    oop java_thread = JNIHandles::resolve_non_null(jthread);
    if (java_thread != NULL) {
      jlong lp = java_lang_Thread::park_event(java_thread);
      if (lp != 0) {
        // This cast is OK even though the jlong might have been read
        // non-atomically on 32bit systems, since there, one word will
        // always be zero anyway and the value set is always the same
        p = (Parker*)addr_from_java(lp);
      } else {
        // Grab lock if apparently null or using older version of library
        MutexLocker mu(Threads_lock);
        java_thread = JNIHandles::resolve_non_null(jthread);
        if (java_thread != NULL) {
          JavaThread* thr = java_lang_Thread::thread(java_thread);
          if (thr != NULL) {
            p = thr->parker();
            if (p != NULL) { // Bind to Java thread for next time.
              java_lang_Thread::set_park_event(java_thread, addr_to_java(p));
            }
          }
        }
      }
    }
  }
  if (p != NULL) {
#ifndef USDT2
    HS_DTRACE_PROBE1(hotspot, thread__unpark, p);
#else /* USDT2 */
    HOTSPOT_THREAD_UNPARK(
                          (uintptr_t) p);
#endif /* USDT2 */
    p->unpark();
  }
UNSAFE_END

點擊查看源碼

前面一大段代碼是在給 Parker* p 賦值,最終調用的是 p 的 unpark 方法:p->unpark()。

(2)os_linux.cpp#Parker::unparkopenjdk/hotspot/src/os/linux/vm/os_linux.cpp
void Parker::unpark() {
  int s, status ;
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ;
  s = _counter;
  _counter = 1;
  if (s < 1) {
    // thread might be parked
    if (_cur_index != -1) {
      // thread is definitely parked
      if (WorkAroundNPTLTimedWaitHang) {
        status = pthread_cond_signal (&_cond[_cur_index]);
        assert (status == 0, "invariant");
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
      } else {
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
        status = pthread_cond_signal (&_cond[_cur_index]);
        assert (status == 0, "invariant");
      }
    } else {
      pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant") ;
    }
  } else {
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

點擊查看源碼

unpark 方法其實很簡單,首先通過 pthread_mutex_lock 獲取鎖,然後將 _counter 置爲 1,再判斷當前是否有線程被掛起,如果有,則通過 pthread_cond_signal 喚醒被掛起的線程,然後釋放鎖。

三、結語

學習知識真是一環扣一環,學一個不會的知識點,很容易碰到新的不會的知識點,然後不斷的接觸新知識點,不斷地學習新的內容;當你不斷的學習,不斷的把新知識點喫透,慢慢的又會發現其實很多知識點的思想又是相通的,學起來反而沒那麼費勁了。

就好比看到 pthread_cond_wait 的機制時,就會聯想到 Object.wait,因爲兩者的實現有很多相像的地方,理解起來也就比較簡單。

最後留一個問題,大家可以思考一下:LockSupport.park 和 Object.wait 兩者有何區別?

參考資料:

(1)由幾個小例引發的對interrupt()、LockSupport.park()深入解析

(2)Class LockSupport

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章