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

2022-09-03 07:18:13

Nio 提供异步 io - 这意味着调用线程在 IO 操作上不会被阻塞。但是,我仍然对此内部工作方式感到困惑?从这个答案 - 只有线程池,其中提交了同步IO。

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

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


答案 1

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

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

所以,假设你已经设置正确了:你有一个8核CPU,你有8个线程运行“等待有趣的东西,句柄套接字与活动数据”循环。

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

坏。因此,不要调用阻止代码。不幸的是,Java中有大量的方法(无论是在java核心库还是第三方库中)阻止。它们往往没有被记录在案。对此没有真正的解决方案。

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

最后,还有性能:为什么要摆脱线程?

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

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

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

但是,非阻塞并不是解决这两个问题的方法:

  1. 当您因为要处理的数据不足而移动到另一个处理程序时,您...还有上下文切换。它不是线程切换,但您仍然需要跳转到完全不同的内存页面,并且在现代体系结构上,访问不在缓存中的内存部分需要很长时间。你只是在用“线程上下文切换”换取“内存页缓存上下文切换”,你什么也没得到。

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

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

因此,务实的结果是这样的:

  1. NIO代码非常难以编写。使用像灰熊或netty这样的抽象,因为它是火箭科学。

  2. 它很少更快。

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

  4. 这有点像使用汇编程序而不是C,因为从技术上讲,你可以从手动进行垃圾回收中挤出更多的性能,而不是让Java为你做这件事。但是有一个原因,大多数人不使用汇编程序来编程,即使它理论上更快。绝大多数Web应用程序都是用java,python,node.js或其他高级语言编写的,而不是像C(++)或汇编程序这样的非托管语言,这是有原因的。


答案 2

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

我创建了一个名为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));

在上面从我的项目中获取的代码片段中,您可以看到它接受一个可以传递自定义的地方(这是一个)。AsynchronousServerSocketChannel#openAsynchronousChannelGroupThreadPoolExecutorExecutorService

因此,为了回答您的问题:是的,线程池用于处理I / O完成,即使使用NIO类也是如此。Asynchronous*

注意:一旦织机项目完成并且Fibers接管了世界,这种情况可能会改变。


推荐