有朋友说过一个场景:调用一个方法,但是该方法可能会一直阻塞下去,所以要设计超时机制,该如何设计?
首先想到的是 Future
接口,其可以在超时的时候抛出超时异常,从而执行超时后的一些处理逻辑。Future
的实现类为 FutrueTask
,其继承关系如下:
可见,每个 FutureTask
对象都实现了 Runnable
接口,也就是每个 FutureTask
都可以作为创建一个新线程的 runnable 参数。
这样子看,Futrue
的异步执行的本质是在当前线程新开一个线程去执行 task 任务,该 task 任务分为两块内容:
比较好奇超时检测是如何实现的,所以决定再看看。
在这里想到了 程序中使用Http请求的时候,应该也会涉及到超时处理,那么它又是怎么处理的呢?这个后续研究。
追踪源码可以看到,在 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()
方法的具体实现到底是什么呢?
在 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 方法进行对比研究,看看它底层是如何实现的。
同样的,Thread.sleep()
,该方法也是 native 方法,其具体实现为:
int os::sleep(Thread* thread, jlong millis, bool interruptible) {
// 一堆代码
slp->park(millis); // 关键
}
可以看到,其本身也是调用 park 方法实现的。
那么,疑问来了,LockSupport.parkNanos
和 Thread.sleep
的区别是什么?存在即合理,所以肯定是有区别的。
上文说的重点在于 定时的处理,现在需要将重点转移到同步的问题上来。
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 中的超时又是如何处理的呢?其涉及到三种超时设置:
ConnectionRequestTimeout
时间第一种池子中获取连接的超时和上文研究的超时比较像,而后两种主要涉及到的是 系统底层 和 网络层的超时处理,情况不太一样,暂不继续研究。
最后想说的是 Linux 中定时器的实现,还是一些数据结构优化问题,感兴趣可以继续看下延伸阅读中的文章,其给出了一般意义上的定时器实现机制,对于嵌入式人员来说也有很大的参考价值。
实际上,定时器系统也是 linux 内核中一个很大的范畴。
实际上本篇引申出了很多可以继续研究的东西,比如:
HttpClient超时浅析
非常有意思的park研究(英文)
Linux 下定时器的实现方式分析