文章目錄
起源
有朋友說過一個場景:調用一個方法,但是該方法可能會一直阻塞下去,所以要設計超時機制,該如何設計?
首先想到的是 Future
接口,其可以在超時的時候拋出超時異常,從而執行超時後的一些處理邏輯。Future
的實現類爲 FutrueTask
,其繼承關係如下:
可見,每個 FutureTask
對象都實現了 Runnable
接口,也就是每個 FutureTask
都可以作爲創建一個新線程的 runnable 參數。
這樣子看,Futrue
的異步執行的本質是在當前線程新開一個線程去執行 task 任務,該 task 任務分爲兩塊內容:
- 返回任務處理結果
- 超時檢測
比較好奇超時檢測是如何實現的,所以決定再看看。
在這裏想到了 程序中使用Http請求的時候,應該也會涉及到超時處理,那麼它又是怎麼處理的呢?這個後續研究。
LockSupport
追蹤源碼可以看到,在 FutureTask 的帶有超時時間參數的 get
方法中,最終在 awaitDone
方法中可以看到:
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos); // 這裏是關鍵的超時處理
}
LockSupport 提供了線程的阻塞等機制,在其 parkNanos
方法實現中,可以看到下面的代碼:
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos); // 這裏是關鍵
setBlocker(t, null);
}
}
該方法中調用了 Unsafe
類中的 park()
方法。Unsafe 類主要負責執行一些不安全的操作,比如自主管理內存的資源等。該 park 方法的聲明如下:
public native void park(boolean b, long l);
可見,這是一個 native
方法,也即本地方法。
方法執行的時候的需要 棧內存 進行操作,那麼對於 native 方法來說,其運行時棧就是 jvm 內存結構中的 本地方法棧
, 那麼 Unsafe 中對於內存的管理,則是針對 堆外內存
的管理。
native 方法:我們可以根據 JNI(java本地接口) 規範來編寫 native 方法的實現,通常使用 C/C++ 編寫,比較偏向底層。
那麼,這裏的 park()
方法的具體實現到底是什麼呢?
park方法實現
在 Linux 平臺上,是使用基於 POSIX 規範的API來實現的park方法。其實現的關鍵代碼如下:
int os::PlatformEvent::park(jlong millis) {
// 忽略一堆
while (_event < 0) {
status = pthread_cond_timedwait(_cond, _mutex, &abst); // 關鍵在這裏
assert_status(status == 0 || status == ETIMEDOUT,
status, "cond_timedwait");
// OS-level "spurious wakeups" are ignored unless the archaic
// FilterSpuriousWakeups is set false. That flag should be obsoleted.
if (!FilterSpuriousWakeups) break;
if (status == ETIMEDOUT) break;
}
}
可以看到,關鍵的超時等待是 pthread_cond_timedwait()
方法,該方法屬於 glibc
庫中的方法。
我們知道,linux 對外支持了許多系統調用,而這些系統調用其實不會被系統開發人員直接使用,大家使用的都是 glibc
中的方法,glibc
相當於對系統調用做了一層封裝。
該方法中最終調用了 futex
這個真正的系統調用,該系統調用中,使用了定時器 hrtimer
這個高精度定時器來實現超時處理。默認地,該定時器 x us計時一次(沒驗證,參考是50)。定時時間到了可以執行對象的中斷處理函數進行後續處理。
至於什麼時候執行 unpark,以及後續動作如何聯動處理,都是通過 信號量
機制來實現各種同步互斥的,這裏暫不關注。
到了這裏,需要對 sleep
這個java 方法進行對比研究,看看它底層是如何實現的。
java sleep 實現原理
同樣的,Thread.sleep()
,該方法也是 native 方法,其具體實現爲:
int os::sleep(Thread* thread, jlong millis, bool interruptible) {
// 一堆代碼
slp->park(millis); // 關鍵
}
可以看到,其本身也是調用 park 方法實現的。
那麼,疑問來了,LockSupport.parkNanos
和 Thread.sleep
的區別是什麼?存在即合理,所以肯定是有區別的。
再說 FutureTask
上文說的重點在於 定時的處理,現在需要將重點轉移到同步的問題上來。
LockSupport
的 park
和 unpark
方法,都需要傳入需要阻塞或者解除阻塞的線程。當A線程調用 get
方法時,其內部的實現上是將 this
作爲參數傳入的,也即將當前的A線程阻塞起來。那麼什麼時候解鎖呢?有兩個觸發方式。
超時解鎖
再仔細看一下上文的 awaitDone
方法:下面已經刪除了一些條件判斷
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (timed) { // 超時判斷
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}
假設在調用 parkNanos
期間任務沒有完成,則會阻塞當前線程,直到定時器計時結束後,再喚醒該線程。從而該線程繼續在 for 循環中判斷 state 狀態,隨即就會發現處於超時狀態,所以上層方法會拋出異常,如下:
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING) // 超時則返回值不會時完成狀態
throw new TimeoutException(); // 所以拋出超時異常
return report(s);
}
還有另一種觸發條件就是任務完成的觸發。
任務完成觸發
我們肯定要先把 FutureTask
任務跑起來纔好執行 get 等方法,所以首先要調用 run 方法,run方法的實現中,最終會在 方法完成後執行如下的代碼:
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null;
q = next;
}
該方法中會對線程執行 unpark 操作喚醒線程,所以可以實現任務完成後 get 方法就會立刻返回。所以 park 和 unpark 的組合使用,可以實現 sleep 實現不了的功能。
小小總結
通過上述研究,可以總結。這種情境下的超時處理不論如何都需要阻塞一個線程。
HttpClient 中的超時
那麼,HttpClient 中的超時又是如何處理的呢?其涉及到三種超時設置:
- ConnectionRequestTimeout:如果配置了連接池,那麼如果池中沒有可用連接,則最多等
ConnectionRequestTimeout
時間 - ConnectTimeout : 拿到連接以後,和通信的另一方建立連接的超時時間。即TCP通道的建立超時。
- SocketTimeout:連接建立以後,數據讀取的超時時間。
第一種池子中獲取連接的超時和上文研究的超時比較像,而後兩種主要涉及到的是 系統底層 和 網絡層的超時處理,情況不太一樣,暫不繼續研究。
linux 定時器的實現
最後想說的是 Linux 中定時器的實現,還是一些數據結構優化問題,感興趣可以繼續看下延伸閱讀中的文章,其給出了一般意義上的定時器實現機制,對於嵌入式人員來說也有很大的參考價值。
實際上,定時器系統也是 linux 內核中一個很大的範疇。
總結
實際上本篇引申出了很多可以繼續研究的東西,比如:
- wait 和 notify 的實現原理對比
- linux內核定時器系統
- JNI實現規範
- 網絡超時的底層原理
- POSIX規範
- 無鎖化編程
- 信號量同步互斥機制
- 等等