okio篇3-超时机制

关于System.nanoTime

System.currentTimeMills与System.nanoTime实际都是时间间隔,只不过两个时间的起始时间衡量不一致。

我们比较常用的,实际是System.currentTimeMills(),这个时间是以1970-01-01起始,到系统显示时间的间隔。

所以,只要改系统时间,这个方法的返回时间就会相应改变。

而System.nanoTime,与系统设置时间无关,同一个jvm中,System.nanoTime的起始是一致的。因此,改变系统时间,也不会改变System.nanoTime,此外,System.nanoTime的时间精度更高。适合用于计算时间间隔。

而在okio中的超时机制,就是使用System.nanoTime来进行计算的。

Timeout

Timeout是一个类,只有3个成员变量需要关注。

  1. timeoutNanos:超时时间,相当于duration
  2. deadlineNanoTime:最终超时时间点,相当于System.nanoTime+timeout=dealineNanoTime
  3. hasDeadLine:是否有deadLine,一般设置了第二个参数,第三个参数就自动为true。

AsyncTimeout

Timeout的子类。

只有4个public方法。enter和exit方法都是在sink和source方法调用的。

有一个静态变量head,还有一个next指针,说明AsyncTimeout维护了一个AsyncTimeout类型的链表。

由于4个public方法中,enter和exit方法都是被source和sink方法调用的,source和sink又是概念相似的方法,所以下面只介绍source方法。

source

public final Source source(final Source source) {
  return new Source() {
    @Override public long read(Buffer sink, long byteCount) throws IOException {
      boolean throwOnTimeout = false;
      enter();// 调用enter
      try {
        long result = source.read(sink, byteCount);// 读取数据
        throwOnTimeout = true;
        return result;
      } catch (IOException e) {
        throw exit(e);
      } finally {
        exit(throwOnTimeout);// 检查是否超时,超时抛异常
      }
    }

    @Override public void close() throws IOException {
      boolean throwOnTimeout = false;
      try {
        source.close();
        throwOnTimeout = true;
      } catch (IOException e) {
        throw exit(e);
      } finally {
        exit(throwOnTimeout);
      }
    }

    @Override public Timeout timeout() {
      return AsyncTimeout.this;
    }

    @Override public String toString() {
      return "AsyncTimeout.source(" + source + ")";
    }
  };
}

source方法很简单,只是在read方法之前,调用enter方法,在read方法之后,调用了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);// 静态方法
}

enter方法只标记了inQueue为true和调用了scheduleTimeout方法。 

private static synchronized void scheduleTimeout(
    AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
  // Start the watchdog thread and create the head node when the first timeout is scheduled.
  if (head == null) {// 创建head指针
    head = new AsyncTimeout();
    new Watchdog().start();// 开启一个线程监听队列中超时的节点
  }

  long now = System.nanoTime();
  if (timeoutNanos != 0 && hasDeadline) {
    // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around,
    // Math.min() is undefined for absolute values, but meaningful for relative ones.
    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();
  }// 求出当前节点的最终超时时间点

  // Insert the node in sorted order.
  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;
      if (prev == head) {
        AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
      }
      break;
    }
  }
}

总结下scheduleTimeout方法:

  1. 调用enter方法时,如果head为null,就创建一个head,并开启一个WatchDog线程(后面会讲)。
  2. 求出当前节点的deadline时间点,和剩余时间。
  3. 根据剩余时间,按序插入链表。
  4. 如果当前链表只有自己一个有效节点(prev == head),就调用AsyncTimeout.class.notify(后面会讲为什么要调用notify)。

WatchDog

private static final class Watchdog extends Thread {
  Watchdog() {
    super("Okio Watchdog");
    setDaemon(true);// 守护线程。不影响JVM退出
  }

  public void run() {
    while (true) {
      try {
        AsyncTimeout timedOut;
        synchronized (AsyncTimeout.class) {
          timedOut = awaitTimeout();

          // Didn't find a node to interrupt. Try again.
          if (timedOut == null) continue;

          // The queue is completely empty. Let this thread exit and let another watchdog thread
          // get created on the next call to scheduleTimeout().
          // 如果等了60s,返回的还是head,就直接不等了,等下一个节点插入的时候,再开始一个新的watchdog线程
          if (timedOut == head) {
            head = null;
            return;
          }
        }

        // Close the timed out node.
        timedOut.timedOut();// 调用超时节点的timeout方法
      } catch (InterruptedException ignored) {
      }
    }
  }
}

总结下WatchDog线程:

  1. WatchDog是一个demon线程,即守护线程。当虚拟机中用户线程数为0时,虚拟机就会退出。而守护线程是不会影响虚拟机退出的。
  2. 调用awaitTimeout获取一个超时的节点,如果节点为null,continue,再重新获取节点。
  3. 如果超时节点为head,就说明当前队列为空,直接退出线程。
  4. 如果超时节点不为空,且不为head,调用节点的timeout方法。

下面看下awaitTimeout是怎么获取一个超时节点的:

static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
  // Get the next eligible node.,head不是一个有效节点,head.next才是第一个有效节点
  AsyncTimeout node = head.next;

  // The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
  if (node == null) {
    long startNanos = System.nanoTime();
    // 等待60s,只有调用enter()的时候才会调用notify,所以这里是检查到没有有效节点的时候,
    // 就等待60s,看有没有新节点插入
    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. 插入了新的节点,返回null,触发WatchDog调用continue,重新回到这个方法
  }

  long waitNanos = node.remainingNanos(System.nanoTime());

  // The head of the queue hasn't timed out yet. Await that.
  if (waitNanos > 0) {
    // Waiting is made complicated by the fact that we work in nanoseconds,
    // but the API wants (millis, nanos) in two arguments.
    long waitMillis = waitNanos / 1000000L;
    waitNanos -= (waitMillis * 1000000L);
    AsyncTimeout.class.wait(waitMillis, (int) waitNanos);// 等待剩余时间,返回null,重新触发调用该方法
    return null;
  }
    // 这个节点的deadLine已经过了,从链表中移除这个节点
  // The head of the queue has timed out. Remove it.
  head.next = node.next;
  node.next = null;
  return node;
}
  1. 如果队列中只有head,即没有有效节点的时候,等待60s。
    •   当线程被唤醒时,只有两种可能,一种是插入了一个新的节点(即上面scheduleTimeout中的AsyncTimeout.class.notify方法),返回null,告诉WatchDog重新获取一遍节点;一种是超时,这时候,head.next还是null,返回head,告诉WatchDog不要再等了,直接退出线程。
  2. 如果head.next不为空
    • 获取节点的剩余时间:remainingNanos
    • 如果remainingNanos>0,就调用wait方法,并再次返回null。
    • 否则就将节点移出队列,并且调用节点的timeout方法。

所以再次总结下,WatchDog干的事情就是,顺序遍历AysncTimeout链表(这大概就是为啥叫AsyncTimeout,开启了个线程专门监听有谁超时了)。如果遍历到有节点超时了,就调用节点的timeout方法。如果没有超时,就调用wait方法,等待节点的剩余时间,再去看链表中有没有超时的节点。

exit

前面介绍的source方法中,enter方法已经介绍完了。enter主要干的事情就是,如果head为空,就创建一个head,并开启一个WatchDog线程监听队列中的超时节点,如果有超时节点,就调用节点的timeout方法。如果head不为空,就获取当前节点的剩余时间,并按顺序插入链表。

这时候再想下,超时节点调用timeout方法很正常,但按WatchDog这么个遍历方法,链表中的所有节点都得超时。所以肯定有个机制,没有超时的节点,要及时移出队列。

所以这就是为什么source.read方法,前有一个enter方法,后有一个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;
}

cancelScheduledTimeout方法,如果队列有这个节点(说明还没超时),就移出这个节点,并返回false,否则返回true。

exit方法,根据cancelScheduledTimeout方法的返回值,如果返回true,会抛出newTimeoutException中定义的exception。

实例:socket

public static Source source(Socket socket) throws IOException {
  if (socket == null) throw new IllegalArgumentException("socket == null");
  if (socket.getInputStream() == null) throw new IOException("socket's input stream == null");
  AsyncTimeout timeout = timeout(socket);// 调用timeout方法。
  Source source = source(socket.getInputStream(), timeout);
  return timeout.source(source);
}
private static AsyncTimeout timeout(final Socket socket) {
  return new AsyncTimeout() {
      // 定义newTimeoutExeception
    @Override protected IOException newTimeoutException(@Nullable IOException cause) {
      InterruptedIOException ioe = new SocketTimeoutException("timeout");
      if (cause != null) {
        ioe.initCause(cause);
      }
      return ioe;
    }
    // 这个方法会在WatchDog检测到已经到达deadline的时候,调用
    // 调用socket.close之后,socket对应的inputStream和outputStream都会被调用close
    // 当inputStream.read方法过程中,inputStream被关闭了,会抛出IOException
    @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;
        }
      }
    }
  };
}

上面一直说,如果WatchDog检测到节点超时了,会调用节点的timeout方法。下面看下timeout方法是如何阻断整个链路的。

在这个socket的示例方法中,timedout方法中调用了socket.close方法。

根据socket.close方法的注释,如果socket被关闭了,socket的inputStream和outputStream都会被调用close方法。

而inputStream被调用close,那么一直等待服务端的inputStream.read方法会被中断,直接抛出IOException。

再看回上面的一个方法source方法:

如果socket被调用close,source.read方法会抛出IOException。

抛出IOException的时候,会再次调用exit方法。大家应该还记得exit方法,是用来看节点还在不在队列中(节点是否超时),如果不在会抛出newTimeoutException方法定义的异常。 

所以,通过上面的例子,就可以知道Source中的timeout方法是用来调用一些关闭资源的方法的。

最后的最后,总结一下:

  1. 当调用AsyncTimeout.source方法时,相当于在原来的source.read方法前后,分别调用了enter和exit方法。enter方法相当于创建了一个节点插入AsyncTimeout维护的超时链表中。而exit方法则是用来将自己从链表中移除。
  2. AsyncTimeout中的WatchDog线程会顺序遍历链表中的节点,如果超时,会调用节点的timeout方法。
  3. Source的timedout方法中,一般会调用close方法,阻断source.read方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值