Java併發程式設計之鎖機制之LockSupport工具

AndyandJennifer發表於2018-11-02

關於文章涉及到的jdk原始碼,這裡把最新的jdk原始碼分享給大家----->jdk原始碼

前言

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

LockSupport類

瞭解執行緒的阻塞和喚醒,我們需要檢視LockSupport類。具體程式碼如下:

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是用來標識當前執行緒等待的物件(下文簡稱為阻塞物件),該物件主要用於問題排查和系統監控

由於在Java 5之前,當執行緒阻塞時(使用synchronized關鍵字)在一個物件上時,通過執行緒dump能夠檢視到該執行緒的阻塞物件。方便問題定位,而Java 5退出的Lock等併發工具卻遺漏了這一點,致使線上程dump時無法提供阻塞物件的資訊。因此,在Java 6中,LockSupport新增了含有阻塞物件的park方法。用以替代原有的park方法。

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)方法怎麼理解呢?

在描述該方法之前,需要給大家講一個知識點。在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深入淺出

相關文章