超时机制在我们的日常生活中随处可见,最为常见就是火车了,如果你不能按时到达火车站点,那么你就错失坐这一趟火车的机会
在前面的文章,就已经提到过 okio 中的有一个超时机制 Timeout
, 现在就来说说它的原理
okio 中的超时机制只要就两种:
- 同步超时
Timeout
- 异步超时
AsyncTimeout
还有一个超时对象 ForwardingTimeout
,不过这个属于一个空盒子,需要装入其他的 Timeout
对象才可以使用
同步超时 TimeOut
所谓超时,就是用来控制某个任务执行的最大时长,当执行超过指定的时间时,任务将被中断
例如当从输入流 Source
读取数据超时后,输入流将被关闭,任务到此结束
而在 Timeout
中,主要使用两个判断条件来判断任务是否超时了:
- 任务设置了结束时间( hasDeadline = true )并且当前已经过了结束时间( deadlineNanoTime )
- 任务已经过了超时时间( timeoutNanos )
正是下面的变量:
public class Timeout {
.....
/**
* 是否设置了结束时间
*/
private boolean hasDeadline;
/**
* 结束时间
*/
private long deadlineNanoTime;
/**
* 超时时间
*/
private long timeoutNanos;
.....
}
在 Timeout
里面,用于判断超时的方法主要是有两个,一个是 throwIfReached
:
/**
*
* 该方法并不是检测超时方法
* 该方法用于检测线程是否中断了或者是否到了结束时间
* 如果是,那么就抛出异常来进行中断
* 目前用于在执行读写操作时的检查
*/
public void throwIfReached() throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException("thread interrupted");
}
//判断是否设置了结束flag以及是否到了结束时间
if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
throw new InterruptedIOException("deadline reached");
}
}
正如上面写的,这个方法是在执行读写操作时,判断一下线程是否中断或当前任务是否已经到了结束时间的
通过对该方法的全局搜索,可以大致明白这个用途:
这时候是不是觉得有点奇怪,怎么还有一个 timeoutNanos
没有用到的?
别急,接下来介绍的 waitUntilNotified
就需要用到它了
waitUntilNotified
用来等待某个指定的 monitor
对象,直到这个对象被 notify 或者超时时间到 为止:
public final void waitUntilNotified(Object monitor) throws InterruptedIOException {
try {
//获取当前是否设置结束flag
boolean hasDeadline = hasDeadline();
//获取超时时间
long timeoutNanos = timeoutNanos();
//当没有设置超时时间,那么就设置 wait ,它将无限等待直到对象被 notify 为止
if (!hasDeadline && timeoutNanos == 0L) {
monitor.wait();
return;
}
//根据timeoutNanos和deadlineNanoTime计算出较短的超时时间waitNanos
//也就是okio需要等待多久
long waitNanos;
//当前系统时间
long start = System.nanoTime();
if (hasDeadline && timeoutNanos != 0) {
//如果设置了结束flag并且超时时间不为0
//先计算下还有多久到结束时间
long deadlineNanos = deadlineNanoTime() - start;
//对比结束时间,超时时间,那个时间段更加短,取短的值
waitNanos = Math.min(timeoutNanos, deadlineNanos);
} else if (hasDeadline) {
waitNanos = deadlineNanoTime() - start;
} else {
waitNanos = timeoutNanos;
}
//调用wait方法并设置等待超时时间
//直到了超时了或者 monitor 给 notify 为止
long elapsedNanos = 0L;
if (waitNanos > 0L) {
long waitMillis = waitNanos / 1000000L;
monitor.wait(waitMillis, (int) (waitNanos - waitMillis * 1000000L));
//记录跑到这里过去了多少时间,用于计算超时
elapsedNanos = System.nanoTime() - start;
}
// 走到这里,说明wait等待超时时间到,或者 monitor 给 notify了
if (elapsedNanos >= waitNanos) {
//如果时超时时间到就抛出InterruptedIOException异常
throw new InterruptedIOException("timeout");
}
} catch (InterruptedException e) {
throw new InterruptedIOException("interrupted");
}
}
这里使用了 Java 的 wait
和 notify
机制,这种常用在同步机制上面
异步超时 AsyncTimeout
异步超时 AsyncTimeout
继承自 TimeOut
,相比起它的父类 TimeOut
,AsyncTimeout
多了一个守护线程 Watchdog
和需要自定义一个 timeOut
方法
例如在 okio 的源码中,就提供了一个自定义 timeOut
方法,用于任务超时后,关闭 Socket
private static AsyncTimeout timeout(final Socket socket) {
return new AsyncTimeout() {
...........
@Override
protected void timedOut() {
try {
socket.close();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) {
// Catch this exception due to a Firmware issue up to android 4.2.2
// https://code.google.com/p/android/issues/detail?id=54072
logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
} else {
throw e;
}
}
}
}
}
了解了 AsyncTimeout
自定义需要注意的事项后,我们来看下它里面的具体原理,先来了解下 Watchdog
这个守护进程到底是干什么的:
可以看到,所有的 AsyncTimeout
在会组成一个链表,而 Watchdog
则是无限循环去取出里面的元素,只要发现超时的元素就会执行 timedOut
方法
链表的定义是在类的开头:
public class AsyncTimeout extends Timeout {
..........
/**
* 链表的头节点,指向链表中第一个元素,也就是head.next为链表的第一个元素
* 如果head.next为null,那么这是一个空的队列
*/
static @Nullable AsyncTimeout head;
/**
* The next node in the linked list.
* 当前 AsyncTimeout 指向的下一个链表元素
*/
private @Nullable AsyncTimeout next;
..........
}
当链表不为空时, 就是 Watchdog
工作的时候了:
private static final class Watchdog extends Thread {
Watchdog() {
super("Okio Watchdog");
setDaemon(true);//设置为守护进程
}
@Override
public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();
//没有找到需要结束的节点,继续循环查找
if (timedOut == null) {
continue;
}
//如果只能找到头指针,那么说明这个队列为null
if (timedOut == head) {
head = null;
return;
}
}
//找到发生超时的元素,执行它的自定义超时回调方法timedOut,
//注意,这里不能做耗时操作,否则会阻塞链表中其他已经发生超时的元素
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
}
首先需要明确的是,守护线程耗费资源是非常低,当只剩下守护线程时,jvm 也就关闭了,因此这里的死循环并没有消耗太多的内存
然后就是 awaitTimeout
,这个方法用来查找当前超时的元素,我们来看下它的怎么去找的:
static AsyncTimeout awaitTimeout() throws InterruptedException {
//获取链表的第一个元素
AsyncTimeout node = head.next;
if (node == null) {
// 链表为空,则最长等待 IDLE_TIMEOUT_NANOS 时间
long startNanos = System.nanoTime();
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
? head // The idle timeout elapsed.
: null; // The situation has changed.
}
long waitNanos = node.remainingNanos(System.nanoTime());
// 链表的第一个元素还没有超时,继续等待超时时间到
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
return null;
}
// 链表的第一个元素超时时间到,从链表中移除并将其返回
head.next = node.next;
node.next = null;
return node;
}
它是通过 head
去找的,这也说明了 链表的排序是按照超时时间的从低到高开始排序的
每次都是去找 head.next
节点,超时了就执行 timeOut
方法,没有就继续 wait
不过,当节点执行了 wait
之后,是什么时候 notify
呢?通过代码追踪,找到了 scheduleTimeout
方法以及 enter
方法 和 exit
方法
enter
方法是进入链表的方法, exit
是退出链表的方法,是执行读写操作都会调用它们:
enter
方法用于插入链表元素 :
public final void enter() {
if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
long timeoutNanos = timeoutNanos();
boolean hasDeadline = hasDeadline();
if (timeoutNanos == 0 && !hasDeadline) {
return; // No timeout and no deadline? Don't bother with the queue.
}
inQueue = true;
scheduleTimeout(this, timeoutNanos, hasDeadline);
}
它最终是调用了 scheduleTimeout
方法:
private static synchronized void scheduleTimeout(
AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
// 当链表为空时,初始化链表并启动守护线程
if (head == null) {
head = new AsyncTimeout();
new Watchdog().start();
}
// 计算超时的时间点timeoutAt
long now = System.nanoTime();
if (timeoutNanos != 0 && hasDeadline) {
node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
} else if (timeoutNanos != 0) {
node.timeoutAt = now + timeoutNanos;
} else if (hasDeadline) {
node.timeoutAt = node.deadlineNanoTime();
} else {
throw new AssertionError();
}
long remainingNanos = node.remainingNanos(now);
//从链表头开始遍历链表
for (AsyncTimeout prev = head; true; prev = prev.next) {
//当到达链表尾部,或者根据超时时间从短到长排序找到合适位置后插入链表
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
node.next = prev.next;
prev.next = node;
//如果当前插入的节点是链表的第一个元素,那么需要唤醒在awaitTimeout方法中的wait操作
if (prev == head) {
//唤醒操作
AsyncTimeout.class.notify();
}
break;
}
}
}
exit
方法用于删除链表中的元素:
final void exit(boolean throwOnTimeout) throws IOException {
boolean timedOut = exit();
if (timedOut && throwOnTimeout) throw newTimeoutException(null);
}
public final boolean exit() {
if (!inQueue) return false;
inQueue = false;
return cancelScheduledTimeout(this);
}
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
// Remove the node from the linked list.
for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
if (prev.next == node) {
prev.next = node.next;
node.next = null;
return false;
}
}
// The node wasn't found in the linked list: it must have timed out!
return true;
}
可以看到,到最后就是简单的链表数据的删除操作
总结
到此,这次 okio 框架的解析就结束了,虽然还有部分功能和模块没有讲到,不过大致的流程原理都已经过了一遍了
对比 Java 的原生 IO,可以看出 okio 使用更加方便,更加高效的内存利用,建议在实际开发中都用上它