okio篇3-超时机制

news2024/9/25 9:29:11

关于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方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/497963.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聚浪成潮,网易数帆CodeWave智能开发平台开启低代码新时代

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 随着全球范围内新一代人工智能技术发展突飞猛进&#xff0c;社会各领域从数字化、网络化向智能化转变&#xff0c;如何进一步释放数据生产力、加速智能化转型已成为企业发展的必修课。 2023年4月25日&#xff0c;“网易数帆…

K8S管理系统项目实战[API开发]-2

后端: gogin 后端代码地址GitHub - yunixiangfeng/k8s-platform: K8s管理系统后端: gogin 5、存储与配置 5.1 ConfigMap 5.2 Secret 5.3 PersistentVolumeClaims 6、工作流 6.1 流程设计 6.2 数据库操作&#xff08;GORM&#xff09; &#xff08;1&#xff09;初始化…

Mysql Sharding-JDBC读写分离 原理

0 课程视频 深入Sharding-JDBC分库分表从入门到精通【黑马程序员】_哔哩哔哩_bilibili 1 基本概念 1.1应用逻辑 1.1.1 msyql 多库 多表 多服务器 1.1.2 通过Sharding-JDBC jar包->增强JDBC 访问多数据源 -> 自动处理成一个数据源 1.1.3 使用数据的人 -> 使用Sh…

Java面试题总结 | Java面试题总结12- 测试模块

测试 测试需要具备的素质 基础的理论知识、编程语言的功底、自动化测试工具、计算机基础知识 业务分析能力&#xff1a;分析业务的流程&#xff0c;分析被测业务数据、分析被测系统的框架、分析业务模块、分析测试所需资源、分析测试完成目标 缺陷洞察能力&#xff1a;一般…

【ChatGPT】吴恩达教程笔记(预备篇)

本文概要 众所周知&#xff0c;吴恩达老师与OpenAI联合推出了一门面向开发者的Prompt课程&#xff08;https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers &#xff09;&#xff0c;时隔几天&#xff0c;吴恩达老师发推说已经有超过20万人…

Netty基础

2.1Netty是什么 是一个基于异步的&#xff08;多线程处理结果和接收&#xff09;、事件驱动的网络应用框架&#xff0c;用于基于快速开发可维护、高性能的网络服务器和客户端 异步是指调用时的异步&#xff0c;他的IO还是多路复用的IO 许多中间件都依赖与Netty zookperhado…

GUITAR PRO8吉他软件好不好用?值不值得下载

所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;想成为一名专业甚至著名的音乐人&#xff0c;用到的工具软件非常多&#xff0c;在众多款软件工具中&#xff0c;Guitar Pro 8能满足乐谱创作者、学习者的所有需要。很多人在听到Guitar Pro这个名词时&#xff0c;本能反应…

spark的RDD算子计算

一、环境配置 import osfrom pyspark import SparkConf, SparkContextif __name__ __main__:os.environ[SPARK_HOME] /export/server/sparkos.environ["PYSPARK_PYTHON"] "/root/anaconda3/envs/pyspark_env/bin/python"os.environ["PYSPARK_DRIV…

JavaWeb07(MVC应用01[家居商城]连接数据库)

目录 一.什么是MVC设计模式&#xff1f; 1.2 MVC设计模式有什么优点&#xff1f; 二.MVC运用&#xff08;家居商城&#xff09; 2.1 实现登录 2.2 绑定轮播【随机三个商品】 2.2.1 效果预览 index.jsp 2.3 绑定最新上架&热门家居 2.3.1 效果预览 2.3.2 代码实现 数据…

linux进程基本知识

1.什么是程序&#xff0c;什么是进程&#xff1f; 程序是静态的概念&#xff0c;例如 gcc xx.c -o pro 磁盘中生成pro文件&#xff0c;叫做程序 进程是程序的一次运行活动&#xff0c;意思是程序跑起来了&#xff0c;系统中就多了一个进程 2.如何查看系统中有哪些进程&…

EMC VNX登录Unisphere错误 certificate has invalid date问题处理

经常有用户反应说&#xff0c;突然用浏览器登录EMC VNX或者Clarrion CX系统的时候出现“certificate has invalid date”的故障&#xff0c;然后无法正常登录图形界面。具体报错如下图所示&#xff1a; 导致这个问题的原因在于VNX系统中的certification认证过期&#xff0c;既然…

SpringBoot整合Echarts实现用户人数和性别展示

一、背景 在Web应用开发中&#xff0c;经常需要使用图表来展示数据&#xff0c;而Echarts是一个非常优秀的图表库。SpringBoot是一个非常流行的Java Web框架&#xff0c;它可以快速搭建Web应用。本文将介绍如何使用SpringBoot集成Echarts&#xff0c;实现展示用户人数和性别的…

百度百科如何创建?创建百度百科的秘诀你值得掌握(经验分享)

百度百科是中国最大的百科全书式的中文网站之一&#xff0c;是广大用户在互联网上获取知识的重要途径之一。任何人都可以在百度百科创建新的词条&#xff0c;为网站的发展作出贡献。 小媒同学将从如何创建百度百科词条和注意事项两个方面来详细介绍百度百科词条的创建流程和相关…

css3 flex弹性布局详解

css3 flex弹性布局详解 一、flexbox弹性盒子 2009年&#xff0c;W3C 提出了一种新的方案----Flex 布局&#xff0c;可以简便、完整、响应式地实现各种页面布局。目前&#xff0c;它已经得到了所有浏览器的支持&#xff0c;这意味着&#xff0c;现在就能很安全地使用这项功能。…

在选择数据库时需要考虑的因素

在文章的第一部分中&#xff0c;我们奠定了理解各种数据库类型及其用例的基础。随着我们继续探索数据库选择的艺术&#xff0c;我们现在将更深入地探讨影响这个决策过程的关键因素。通过更详细地检查每个因素&#xff0c;我们可以更好地装备自己做出符合项目要求的知情选择&…

ubuntu 卸载 软件包 libfdt-dev

编译环境 Win10 64位 ubuntu 20.04 虚拟机 VMware Workstation 16 Pro NUC980 bsp 自带的编译工具链&#xff1a; nuc980bsp.tar.gz&#xff0c;版本 gcc version 4.8.4 (GCC) NUC980 uboot : https://gitee.com/OpenNuvoton/NUC970_U-Boot_v2016.11 目标 手动编译 NUC9…

【Windows】Windows下载使用wget命令

文章目录 一、前言 & 介绍二、安装步骤2.1 下载 wget 压缩包2.2 解压到指定的位置2.3 检查是否安装成功2.4 是否可以正常下载 一、前言 & 介绍 wget 是 linux 一个下载文件的工具&#xff0c;可以下载一些软件或从远程服务器恢复备份到本地服务器。 wget 在 Linux 下…

PCA主成成分分析例题详解

主成分分析是一种降维算法&#xff0c;它能将多个指标转换为少数几个主成分&#xff0c;这些主成分是原始变量的线性组合&#xff0c;且彼此之间互不相关&#xff0c;其能反映出原始数据的大部分信息 需要了解具体细节可看此视频&#x1f449;&#xff1a;什么是主成成分分析PC…

webpack 5 实战(3)

四十一、代码拆分方式 通过Webpack实现前端项目整体模块化的优势很明显&#xff0c;但是它同样存在一些弊端&#xff0c;那就是项目当中所有的代码最终都会被打包到一起&#xff0c;试想一下&#xff0c;如果说应用非常复杂&#xff0c;模块非常多的话&#xff0c;那打包结果就…

C++ ---- 入门基础知识总结

思维导图 目录 命名空间 命名冲突 如何定义命名空间 命名空间定义语法 嵌套定义 同一工程下定义同名命名空间 命名空间的使用 命名空间名称和作用域限定符&#xff08;: :&#xff09; using将命名空间中某个成员“释放”出来 using namespace 命名空间名称 C标准库…