Java併發編程之鎖機制之LockSupport工具

關於文章涉及到的jdk源碼,這裏把最新的jdk源碼分享給你們----->jdk源碼html

前言

在上篇文章《Java併發編程之鎖機制之AQS(AbstractQueuedSynchronizer)》中咱們瞭解了整個AQS的內部結構,與其獨佔式與共享式獲取同步狀態的實現。可是並無詳細描述線程是如何進行阻塞與喚醒的。我也提到了線程的這些操做都與LockSupport工具類有關。如今咱們就一塊兒來探討一下該類的具體實現。java

LockSupport類

瞭解線程的阻塞和喚醒,咱們須要查看LockSupport類。具體代碼以下:linux

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    private static void setBlocker(Thread t, Object arg) {
        U.putObject(t, PARKBLOCKER, arg);
    }
    
    public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);
        setBlocker(t, null);
    }

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            U.park(false, nanos);
            setBlocker(t, null);
        }
    }

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(true, deadline);
        setBlocker(t, null);
    }

 
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return U.getObjectVolatile(t, PARKBLOCKER);
    }

    public static void park() {
        U.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            U.park(false, nanos);
    }

    public static void parkUntil(long deadline) {
        U.park(true, deadline);
    }

    //省略部分代碼
    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long PARKBLOCKER;
    private static final long SECONDARY;
    static {
        try {
            PARKBLOCKER = U.objectFieldOffset
                (Thread.class.getDeclaredField("parkBlocker"));
            SECONDARY = U.objectFieldOffset
                (Thread.class.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

}
複製代碼

從上面的代碼中,咱們能夠知道LockSupport中的對外提供的方法都是靜態方法。這些方法提供了最基本的線程阻塞和喚醒功能,在LockSupport類中定義了一組以park開頭的方法用來阻塞當前線程。以及unPark(Thread thread)方法來喚醒一個被阻塞的線程。關於park開頭的方法具體描述以下表所示:編程

park.png

其中park(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline)三個方法是Java 6中新增長的方法。其中參數blocker是用來標識當前線程等待的對象(下文簡稱爲阻塞對象),該對象主要用於問題排查和系統監控windows

因爲在Java 5以前,當線程阻塞時(使用synchronized關鍵字)在一個對象上時,經過線程dump可以查看到該線程的阻塞對象。方便問題定位,而Java 5退出的Lock等併發工具卻遺漏了這一點,導致在線程dump時沒法提供阻塞對象的信息。所以,在Java 6中,LockSupport新增了含有阻塞對象的park方法。用以替代原有的park方法。bash

LockSupport中的blocker

可能有不少讀者對Blocker的原理有點好奇,既然線程都被阻塞了,是經過什麼辦法將阻塞對象設置到線程中去的呢? 不急不急,咱們繼續查看含有阻塞對象(Object blocker)的park方法。 咱們發現內部都調用了setBlocker(Thread t, Object arg)方法。具體代碼以下所示:多線程

private static void setBlocker(Thread t, Object arg) {
        U.putObject(t, PARKBLOCKER, arg);
    }
複製代碼

其中 U爲sun.misc.包下的Unsafe類。而其中的PARKBLOCKER是在靜態代碼塊中進行賦值的,也就是以下代碼:併發

private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  static {
        try {
            PARKBLOCKER = U.objectFieldOffset
                (Thread.class.getDeclaredField("parkBlocker"));
		   //省略部分代碼
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }
複製代碼

Thread.class.getDeclaredField("parkBlocker")方法其實很好理解,就是獲取線程中的parkBlocker字段。若是有則返回其對應的Field字段,若是沒有則拋出NoSuchFieldException異常。那麼關於Unsafe中的objectFieldOffset(Field f)方法怎麼理解呢?dom

在描述該方法以前,須要給你們講一個知識點。在JVM中,能夠自由選擇如何實現Java對象的"佈局",也就Java對象的各個部分分別放在內存那個地方,JVM是能夠感知和決定的。 在sun.misc.Unsafe中提供了objectFieldOffset()方法用於獲取某個字段相對 Java對象的「起始地址」的偏移量,也提供了getInt、getLong、getObject之類的方法可使用前面獲取的偏移量來訪問某個Java 對象的某個字段。異步

有可能你們理解起來比較困難,這裏給你們畫了一個圖,幫助你們理解,具體以下圖所示:

blocker.png

在上圖中,咱們建立了兩個Thread對象,其中Thread對象1在內存中分配的地址爲0x10000-0x10100,Thread對象2在內存中分配的地址爲0x11000-0x11100,其中parkBlocker對應內存偏移量爲2(這裏咱們假設相對於其對象的「起始位置」的偏移量爲2)。那麼經過objectFieldOffset(Field f)就能獲取該字段的偏移量。須要注意的是某字段在其類中的內存偏移量老是相同的,也就是對於Thread對象1與Thread對象2,parkBlocker字段在其對象所在的內存偏移量始終是相同的。

那麼咱們再回到setBlocker(Thread t, Object arg)方法,當咱們獲取到parkBlocker字段在其對象內存偏移量後, 接着會調用U.putObject(t, PARKBLOCKER, arg);,該方法有三個參數,第一個參數是操做對象,第二個參數是內存偏移量,第三個參數是實際存儲值。該方法理解起來也很簡單,就是操做某個對象中某個內存地址下的數據。那麼結合咱們上面所講的。該方法的實際操做結果以下圖所示:

blocker_set.png

到如今,咱們就應該懂了,儘管當前線程已經阻塞,可是咱們仍是能直接操控線程中實際存儲該字段的內存區域來達到咱們想要的結果。

LockSupport底層代碼實現

經過閱讀源代碼咱們能夠發現,LockSupport中關於線程的阻塞和喚醒,主要調用的是sun.misc.Unsafe 中的park(boolean isAbsolute, long time)unpark(Object thread)方法,也就是以下代碼:

private static final jdk.internal.misc.Unsafe theInternalUnsafe =   
      jdk.internal.misc.Unsafe.getUnsafe();
      
	public void park(boolean isAbsolute, long time) {
        theInternalUnsafe.park(isAbsolute, time);
    }
    public void unpark(Object thread) {
        theInternalUnsafe.unpark(thread);
    }
複製代碼

查看sun.misc.包下的Unsafe.java文件咱們能夠看出,內部其實調用的是jdk.internal.misc.Unsafe中的方法。繼續查看jdk.internal.misc.中的Unsafe.java中對應的方法:

@HotSpotIntrinsicCandidate
    public native void unpark(Object thread);

    @HotSpotIntrinsicCandidate
    public native void park(boolean isAbsolute, long time);
複製代碼

經過查看方法,咱們能夠得出最終調用的是JVM中的方法,也就是會調用hotspot.share.parims包下的unsafe.cpp中的方法。繼續跟蹤。

UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  //省略部分代碼
  thread->parker()->park(isAbsolute != 0, time);
  //省略部分代碼
} UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
  Parker* p = NULL;
  //省略部分代碼
  if (p != NULL) {
    HOTSPOT_THREAD_UNPARK((uintptr_t) p);
    p->unpark();
  }
} UNSAFE_END
複製代碼

經過觀察代碼咱們發現,線程的阻塞和喚醒實際上是與hotspot.share.runtime中的Parker類相關。咱們繼續查看:

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;//該變量很是重要,下文咱們會具體描述
	 //省略部分代碼
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();
  //省略部分代碼

}
複製代碼

在上述代碼中,volatile int _counter該字段的值很是重要,必定要注意其用volatile修飾(在下文中會具體描述,接着當咱們經過SourceInsight工具(推薦你們閱讀代碼時,使用該工具)點擊其park與unpark方法時,咱們會獲得以下界面:

parker.png

從圖中紅色矩形中咱們可也看出,針對線程的阻塞和喚醒,不一樣操做系統有着不一樣的實現。衆所周知Java是跨平臺的。針對不一樣的平臺,作出不一樣的處理。也是很是理解的。由於做者對windows與solaris操做系統不是特別瞭解。因此這裏我選擇對Linux下的平臺下進行分析。也就是選擇hotspot.os.posix包下的os_posix.cpp文件進行分析。

Linux下的park實現

爲了方便你們理解Linux下的阻塞實現,在實際代碼中我省略了一些不重要的代碼,具體以下圖所示:

void Parker::park(bool isAbsolute, jlong time) {

  //(1)若是_counter的值大於0,那麼直接返回
  if (Atomic::xchg(0, &_counter) > 0) return;
    
  //獲取當前線程
  Thread* thread = Thread::current();
  JavaThread *jt = (JavaThread *)thread;
  
  //(2)若是當前線程已經中斷,直接返回。
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  //(3)判斷時間,若是時間小於0,或者在絕對時間狀況下,時間爲0直接返回
  struct timespec absTime;
  if (time < 0 || (isAbsolute && time == 0)) { // don't wait at all return; } //若是時間大於0,判斷阻塞超時時間或阻塞截止日期,同時將時間賦值給absTime if (time > 0) { to_abstime(&absTime, time, isAbsolute); } //(4)若是當前線程已經中斷,或者申請互斥鎖失敗,則直接返回 if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) { return; } //(5)若是是時間等於0,那麼就直接阻塞線程, if (time == 0) { _cur_index = REL_INDEX; // arbitrary choice when not timed status = pthread_cond_wait(&_cond[_cur_index], _mutex); assert_status(status == 0, status, "cond_timedwait"); } //(6)根據absTime以前計算的時間,阻塞線程相應時間 else { _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX; status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime); assert_status(status == 0 || status == ETIMEDOUT, status, "cond_timedwait"); } //省略部分代碼 //(7)當線程阻塞超時,或者到達截止日期時,直接喚醒線程 _counter = 0; status = pthread_mutex_unlock(_mutex); //省略部分代碼 } 複製代碼

從整個代碼來看其實關於Linux下的park方法分爲如下七個步驟:

  • (1)調用Atomic::xchg方法,將_counter的值賦值爲0,其方法的返回值爲以前_counter的值,若是返回值大於0(由於有其餘線程操做過_counter的值,也就是其餘線程調用過unPark方法),那麼就直接返回。
  • (2)若是當前線程已經中斷,直接返回。也就是說若是當前線程已經中斷了,那麼調用park()方法來阻塞線程就會無效。
  • (3) 判斷其設置的時間是否合理,若是合理,判斷阻塞超時時間阻塞截止日期,同時將時間賦值給absTime
  • (4) 在實際對線程進行阻塞前,再一次判斷若是當前線程已經中斷,或者申請互斥鎖失敗,則直接返回
  • (5) 若是是時間等於0(時間爲0,表示一直阻塞線程,除非調用unPark方法喚醒),那麼就直接阻塞線程,
  • (6)根據absTime以前計算的時間,並調用pthread_cond_timedwait方法阻塞線程相應的時間。
  • (7) 當線程阻塞相應時間後,經過pthread_mutex_unlock方法直接喚醒線程,同時將_counter賦值爲0。

由於關於Linux的阻塞涉及到其內部函數,這裏將用到的函數都進行了聲明。你們能夠根據下表所介紹的方法進行理解。具體方法以下表所示:

linux方法.png

Linux下的unpark實現

在瞭解了Linux的park實現後,再來理解Linux的喚醒實現就很是簡單了,查看相應方法:

void Parker::unpark() {
  int status = pthread_mutex_lock(_mutex);
  assert_status(status == 0, status, "invariant");
  const int s = _counter;
  //將_counter的值賦值爲1
  _counter = 1;
  // must capture correct index before unlocking
  int index = _cur_index;
  status = pthread_mutex_unlock(_mutex);
  assert_status(status == 0, status, "invariant");
  //省略部分代碼
}
複製代碼

其實從代碼總體邏輯來說,最終喚醒其線程的方法爲pthread_mutex_unlock(_mutex)(關於該函數的做用,我已經在上表進行介紹了。你們能夠參照Linux下的park實現中的圖表進行理解)。同時將_counter的值賦值爲1, 那麼結合咱們上文所講的park(將線程進行阻塞)方法,那麼咱們能夠得知整個線程的喚醒與阻塞,在Linux系統下,實際上是受到Parker類中的_counter的值的影響的

LockSupport的使用

如今咱們基本瞭解了LockSupport的基本原理。如今咱們來看看它的基本使用吧。在例子中,爲了方便你們順便弄清blocker的做用,這裏我調用了帶blocker的park方法。具體代碼以下所示:

class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                LockSupport.park("線程a的blocker數據");
                System.out.println("我是被線程b喚醒後的操做");
            }
        });
        a.start();

        //讓當前主線程睡眠1秒,保證線程a在線程b以前執行
        Thread.sleep(1000);
        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                
                String before = (String) LockSupport.getBlocker(a);
                System.out.println("阻塞時從線程a中獲取的blocker------>" + before);
                LockSupport.unpark(a);
                
                //這裏睡眠是,保證線程a已經被喚醒了
                try {
                    Thread.sleep(1000);
                    String after = (String) LockSupport.getBlocker(a);
                    System.out.println("喚醒時從線程a中獲取的blocker------>" + after);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        });
        b.start();
    }

}

複製代碼

代碼中,建立了兩個線程,線程a與線程b(線程a優先運行與線程b),在線程a中,經過調用LockSupport.park("線程a的blocker數據");給線程a設置了一個String類型的blocker,當線程a運行的時候,直接將線程a阻塞。在線程b中,先會獲取線程a中的blocker,打印輸出後。再經過LockSupport.unpark(a);喚醒線程a。當喚醒線程a後。最後輸出並打印線程a中的blocker。 實際代碼運行結果以下:

阻塞時從線程a中獲取的blocker------>線程a的blocker數據
我是被線程b喚醒後的操做
喚醒時從線程a中獲取的blocker------>null
複製代碼

從結果中,咱們能夠看出,線程a被阻塞時,後續就不會再進行操做了。當線程a被線程b喚醒後。以前設置的blocker也變爲null了。同時若是在線程a中park語句後還有額外的操做。那麼會繼續運行。關於爲毛以前的blocker以前變爲null,具體緣由以下:

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);//當線程被阻塞時,會阻塞在這裏
        setBlocker(t, null);//線程被喚醒時,會將blocer置爲null
    }
複製代碼

經過上述例子,咱們徹底知道了blocker能夠在線程阻塞的時候,獲取數據。也就證實了當咱們對線程進行問題排查和系統監控的時候blocker的有着很是重要的做用。

最後

該文章參考如下博客,站在巨人的肩膀上。能夠看得更遠。

Linux 多線程 - 線程異步與同步機制

LockSupport解析與使用

本身動手寫把」鎖」---LockSupport深刻淺出

相關文章
相關標籤/搜索