java NIO内部如何工作,内部使用线程池吗?

Posted

技术标签:

【中文标题】java NIO内部如何工作,内部使用线程池吗?【英文标题】:How does java NIO work internally, is thread pool used internally? 【发布时间】:2019-06-26 06:54:39 【问题描述】:

Nio 提供异步 io - 这意味着调用线程不会在 IO 操作上被阻塞。但是,我仍然很困惑这在内部是如何工作的? 从这个答案 - 只有提交同步 IO 的线程池。

jvm 是否有实际执行同步 IO 的线程池? Linux 有本机 AIO 支持——java 是否在内部使用它。 AIO 如何在操作系统级别工作 - 它是否有线程池但在操作系统级别 - 或者有一些魔法根本不需要线程?

一般来说,问题是 - 异步 NIO 是否让我们能够获得线程绑定 - 或者它只是同步 IO 的包装器,允许我们拥有固定数量的线程来执行 IO

【问题讨论】:

【参考方案1】:

问题“java NIO 内部是如何工作的?”对于 *** 来说太宽泛了,但是关于线程池的问题却不是。

我创建了一个名为SimpleNet 的网络框架,我想用它作为示例来回答您的问题,因为它使用了AsynchronousServerSocketChannelAsynchronousSocketChannel 等类。

executor = new ThreadPoolExecutor(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), runnable -> 
    Thread thread = new Thread(runnable);
    thread.setDaemon(false);
    return thread;
);

executor.prestartAllCoreThreads();

channel = AsynchronousServerSocketChannel.open(AsynchronousChannelGroup.withThreadPool(executor));

在上述从我的项目中提取的代码 sn-p 中,您可以看到 AsynchronousServerSocketChannel#open 接受 AsynchronousChannelGroup,您可以在其中传递自定义 ThreadPoolExecutor(这是 ExecutorService)。

所以回答你的问题:是的,线程池用于处理 I/O 完成,即使是 Asynchronous* NIO 类。

注意:一旦 Project Loom 完成并且 Fibers 占领了世界,这种情况可能会发生变化。

【讨论】:

感谢您的回答,但我仍然不确定您是否正确。根据 javadoc docs.oracle.com/javase/7/docs/api/java/nio/channels/…,据我了解,此线程池用于运行完成处理程序 docs.oracle.com/javase/7/docs/api/java/nio/channels/… - 用于处理 IO 事件而不是实际 IO 的处理程序。正如我在 node.js 中理解的那样,我们有单个事件循环 - 在 java 中,我们能够在这个线程池的线程中同时运行回调。 @OleksandrPapchenko 是的,实际的 I/O 由操作系统处理。 所以这个 ThreadPool 中的线程必须等到操作系统处理实际的 I/O ?【参考方案2】:

内核本身(无论是 windows 还是 linux 或其他更奇特的东西)负责执行非阻塞 I/O,而 nio 包中的 java 类(例如 Channel 和 Selector)只是非常低级的转换那个 API。

低级别的东西需要你创建线程才能正确地完成它。 java.* 中的基本 NIO 支持本身允许您调用一个方法,该方法会阻塞,直到您感兴趣的至少一件事情发生在任意数量的批处理非阻塞通道上。例如,您可以有 1000 个代表网络套接字的开放通道都在等待“如果某些网络数据包到达这 1000 个开放套接字中的任何一个,我很感兴趣”,然后调用一个方法说:“请睡觉直到发生有趣的事情” .如果您将应用程序设置为调用此方法,然后处理所有有趣的事情,然后返回调用此方法,那么您编写的应用程序效率相当低:CPU 往往有多个内核,除一个之外的所有内核都处于休眠状态什么都不做。正确的模型是让多个线程(每个内核或多或少一个)都运行相同的“用有趣的事物列表唤醒我”模型。除非您故意编写性能不佳的代码,否则您无法摆脱线程。

因此,假设您设置正确:您有一个 8 核 CPU,并且您有 8 个线程运行“等待有趣的东西,handle-sockets-with-active-data”循环。

想象一下您的句柄套接字代码块的一部分。也就是说,它会做一些会导致 CPU 去检查其他作业要做的事情,因为它必须等待,比如说,网络、磁盘或类似的东西。假设因为您已经在其中放置了一些数据库查询,但您没有意识到数据库查询使用(可能是本地的,但仍然是)网络并访问磁盘。那真的很糟糕:你有足够的 CPU 资源来处理这 1000 个传入的请求,但是你的整个 8 个线程集都在等待数据库做事情,虽然 CPU 可以分析数据包和响应,但它有没有什么可做的,并且在等待数据库从磁盘获取记录所需的时间时停止。

不好。所以,NOT 调用阻塞代码。不幸的是,java中有很多方法(在java核心库和第三方库中)会阻塞。它们往往没有记录在案。对此没有真正的解决方案。

有些库确实提供了解决方案,但如果提供,则必须采用“回调”形式:以 DB 查询为例:您需要做的是获取该网络套接字,告诉它您是,至少现在,不再对传入数据感兴趣(您已经在等待数据库响应,尝试为此套接字处理更多传入数据没有意义);相反,您想将数据库连接本身关联(并且 NIO api 本身不支持,您必须构建某种框架)数据库连接本身,因为“如果此数据库查询已准备好响应,我很感兴趣”。 Java 作为一种语言不适合以这种方式编写,您最终会遇到“回调地狱”,这就是 javascript 的工作方式。回调地狱有解决方案,但仍然很复杂,java基本上不支持它们(例如,'yield'是一个可以提供帮助的东西。Java不支持yield概念)。

最后是性能:为什么你想摆脱线程?

线程会受到 2 个主要惩罚:

    上下文切换。当 CPU 必须跳转到另一个线程时(因为它所在的线程需要等待磁盘或网络数据,因此现在无事可做),它需要跳转到另一个代码位置并确定要加载哪些内存表进入缓存运行它。

    堆栈。就像几乎每个编程模型一样,有一个称为“堆栈”的内存,其中包含局部变量和调用你的方法的位置(以及调用它的方法,一直到你的主方法/线程运行方法)。如果你得到一个堆栈跟踪,你正在查看它的效果。在java中,每个线程都获得1个堆栈,并且所有堆栈的大小相同。您可以使用-Xss JVM 参数对其进行配置,最小值为 1MB。这意味着,如果您同时需要 4000 个线程,那么堆栈的价值是 4GB,这是无法避免的(然后您需要更多的内存用于堆等)。

但是,非阻塞并不能很好地解决这些问题:

    当移动到另一个处理程序时,因为您已经用完了要处理的数据,您……也进行了上下文切换。这不是线程切换,但您仍然需要跳转到完全不同的内存页面,并且在现代架构上,访问不在缓存中的内存部分需要很长时间。你只是在用'线程上下文切换'来换取'内存页面缓存上下文切换',而你一无所获。

    假设您是某种聊天应用程序,并且您已从其中一个连接的客户端收到要发送的消息。您现在需要查询数据库以查看此用户是否有权将此消息发布到它打算发送到的聊天频道,并查看是否还有其他需要更新的跟随模式设备。因为这是一个阻塞操作,您希望在等待时跳到另一个工作。但是您需要在某个地方记住这种状态:发送用户、消息、数据库查询的结果。在线程模型中,这些数据会自动和隐式地为您处理:它在那个堆栈空间中。如果你使用完整的 NIO,你需要自己管理这个,例如使用 ByteBuffers。

是的,当您手动控制字节缓冲区时,您可以将它们精确到需要的大小,并且通常会远小于 1MB,因此您可以通过这种方式处理更多的同时连接。或者,您只需在服务器中放入 64GB 的 RAM。

那么,务实的结果是:

    NIO 代码极难编写。使用 grizzly 或 netty 之类的抽象,因为它是火箭科学。

    很少会更快。

    您可以同时进行更多的事情,如果需要跟踪连接/文件/作业/等的数据量很低。

    这有点像使用汇编程序而不是 C,因为从技术上讲,您可以通过手动执行垃圾收集而不是让 java 为您完成这项工作,从而获得更多性能。但是大多数人不使用汇编程序来编写东西是有原因的,即使理论上它更快。绝大多数 Web 应用程序都是用 java、python、node.js 或其他高级语言编写的,而不是像 C(++) 或汇编程序这样的非托管语言,这是有原因的。

【讨论】:

嘿,你有学习过的资源吗,我查看了与 nio 相关的 java 文档,但仍然不清楚它是如何与内核交互的,提前谢谢 你检查JDK源代码和Hotspot JVM源代码。 这不是一个答案,因为它没有回答这个 API 是否使用线程池的问题 @oᴉɹǝɥɔ 第一句话,说的是底层内核做的。我假设有足够的java知识。具体来说,知道 ThreadPool 是一个 java 类,因此第一行立即回答了这个问题:不,它没有。

以上是关于java NIO内部如何工作,内部使用线程池吗?的主要内容,如果未能解决你的问题,请参考以下文章

关于node.js内部异步I/O机制的困惑

j2ee开发中,需要使用线程、线程池吗?在啥时候用线程?啥时候用线程池?

WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用

Java NIO 选择器(Selector)的内部实现(poll epoll)

Netty—— 概念剖析(零拷贝)

Netty—— 概念剖析(零拷贝)