高效易用的okio(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/f409031mn/article/details/85850732

超时机制在我们的日常生活中随处可见,最为常见就是火车了,如果你不能按时到达火车站点,那么你就错失坐这一趟火车的机会

在前面的文章,就已经提到过 okio 中的有一个超时机制 Timeout, 现在就来说说它的原理

okio 中的超时机制只要就两种:

  1. 同步超时 Timeout
  2. 异步超时 AsyncTimeout

还有一个超时对象 ForwardingTimeout ,不过这个属于一个空盒子,需要装入其他的 Timeout 对象才可以使用

同步超时 TimeOut

所谓超时,就是用来控制某个任务执行的最大时长,当执行超过指定的时间时,任务将被中断

例如当从输入流 Source 读取数据超时后,输入流将被关闭,任务到此结束

而在 Timeout 中,主要使用两个判断条件来判断任务是否超时了:

  1. 任务设置了结束时间( hasDeadline = true )并且当前已经过了结束时间( deadlineNanoTime )
  2. 任务已经过了超时时间( 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 的 waitnotify 机制,这种常用在同步机制上面

异步超时 AsyncTimeout

异步超时 AsyncTimeout 继承自 TimeOut ,相比起它的父类 TimeOutAsyncTimeout 多了一个守护线程 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 使用更加方便,更加高效的内存利用,建议在实际开发中都用上它

猜你喜欢

转载自blog.csdn.net/f409031mn/article/details/85850732
今日推荐