Nio提供了异步io-意味着在IO操作上不会阻塞调用线程。但是,我仍然对此感到困惑。从这个答案-只有线程池,其中同步IO提交。
jvm是否具有实际执行同步IO的线程池?Linux有本机AIO支持-Java是否在内部使用它。AIO是如何在OS级别上工作的-它是否具有线程池,但在OS级别上-还是有些魔术根本不需要线程?
总的来说,问题是-做异步NIO可以使我们获得线程绑定的能力吗?或者,它只是封装在同步IO周围,从而使我们可以使用固定数量的线程来执行IO
内核本身(无论是 windows 或 linux 还是更奇特的东西)负责执行非阻塞 I/O,而 nio 包中的 java 类(例如 Channel 和 Selector)只是该 API 的相当低级的翻译.
低级的东西需要你制作线程才能正确地做。java.* 中的基本 NIO 支持本身允许您调用一个方法,该方法会阻塞,直到您感兴趣的至少一件事情发生在任意数量的批处理非阻塞通道上。例如,您可以有 1000 个表示网络套接字的开放通道,它们都在等待“如果某些网络数据包到达这 1000 个开放套接字中的任何一个,我很感兴趣”,然后调用一个方法说:“请睡觉直到发生有趣的事情” . 如果你设置你的应用程序来调用这个方法,然后处理所有有趣的事情,然后回到调用这个方法,你就编写了一个相当低效的应用程序:CPU 往往有多个内核,除了一个内核之外,其他所有内核都处于休眠状态什么都不做。正确的模型是让多个线程(每个内核或多或少一个)都运行相同的“用一系列有趣的事情唤醒我”模型。除非您故意编写性能不佳的代码,否则您无法摆脱线程。
因此,假设您已经正确设置了它:您有一个 8 核 CPU,并且有 8 个线程运行“等待有趣的东西,处理套接字和活动数据”循环。
想象一下句柄套接字代码块的一部分。也就是说,它会做一些会导致 CPU 去检查其他作业要做的事情,因为它必须等待,比如说,网络,或磁盘,或类似的东西。假设您已经在其中放置了一些数据库查询,但您没有意识到数据库查询使用(可能是本地的,但仍然是)网络并访问磁盘。那真的很糟糕:您有足够的 CPU 资源来处理那 1000 个传入请求,但是您的整个 8 个线程集都在等待 DB 执行操作,而 CPU 可以分析数据包和响应,它有没有什么可做的,并且会减少等待数据库从磁盘获取记录所需的时间。
坏的。所以,不要调用阻塞代码。不幸的是,Java 中有大量方法(在 Java 核心库和第三方库中)会阻塞。他们往往没有被记录在案。对此没有真正的解决方案。
一些图书馆确实提供了解决方案,但如果他们这样做,则必须采用“回调”形式:以数据库查询为例:您必须做的是获取该网络套接字,至少告诉它您是现在,不再对传入数据感兴趣(您已经在等待 DB 响应,尝试为此套接字处理更多传入数据毫无意义);相反,您想要关联(并且 NIO api 本身不支持此功能,您必须构建某种框架)数据库连接本身,因为“如果此数据库查询已准备好响应,我很感兴趣”。Java 作为一种语言不适合以这种方式编写,你最终会遇到“回调地狱”,这就是 javascript 的工作方式。有回调地狱的解决方案,但它仍然很复杂,
最后,还有性能:为什么要摆脱线程?
线程会导致 2 个主要惩罚:
上下文切换。当 CPU 必须跳转到另一个线程时(因为它所在的线程需要等待磁盘或网络数据,因此现在无事可做),它需要跳转到另一个代码位置并找出要加载的内存表进入缓存运行它。
堆栈。就像几乎所有的编程模型一样,有一点称为“堆栈”的内存,其中包含局部变量和调用您的方法的位置(以及调用它的方法,一直到您的主方法/线程运行)方法)。如果您获得堆栈跟踪,您正在查看它的效果。在java中,每个线程有1个栈,所有栈的大小相同。您可以使用-XssJVM 参数对其进行配置,最小值为 1MB。意思是,如果你想要同时有 4000 个线程,那么这就是 4GB 的堆栈,这是不可避免的(然后你需要更多的堆内存等等)。
但是,非阻塞并不能很好地解决这两个问题:
当因为要处理的数据用完而移动到另一个处理程序时,您……还需要进行上下文切换。这不是线程切换,但您仍然需要跳转到完全不同的内存页面,并且在现代架构中,访问不在缓存中的部分内存需要很长时间。您只是用“线程上下文切换”换取“内存页面缓存上下文切换”,而您一无所获。
假设您是某种聊天应用程序,并且您已从连接的客户端之一收到要发送的消息。您现在需要查询数据库以查看此用户是否有权将此消息发布到它打算将其发送到的聊天频道,并查看是否还有任何其他需要更新的跟随模式设备。因为这是一个阻塞操作,你想在等待时跳到另一个工作。但是你需要在某个地方记住这个状态:发送用户、消息、数据库查询的结果。在线程模型中,这些数据会自动且隐式地为您处理:它在该堆栈空间中。如果你使用完整的 NIO,你需要自己管理它,例如使用 ByteBuffers。
是的,当您手动控制字节缓冲区时,您可以将它们精确地设置为所需的大小,并且通常远小于 1MB,因此您可以通过这种方式处理更多的并发连接。或者,您只需在服务器中放入 64GB 内存条。
那么,务实的结果是:
NIO 代码极难编写。使用 grizzly 或 netty 之类的抽象,因为它是火箭科学。
它很少更快。
如果需要为连接/文件/作业/等跟踪的数据量很少,您可以同时进行更多的事情。
这有点像使用汇编程序而不是 C,因为从技术上讲,您可以通过手动进行垃圾收集而不是让 java 为您完成更多的性能。但是大多数人不使用汇编程序来编程是有原因的,即使它在理论上更快。绝大多数 Web 应用程序都是用 java、python、node.js 或其他高级语言编写的,而不是像 C(++) 或汇编程序这样的非托管语言,这是有原因的。
问题“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));
Run Code Online (Sandbox Code Playgroud)
在上面从我的项目中获取的代码片段中,您可以看到它AsynchronousServerSocketChannel#open接受一个AsynchronousChannelGroup可以传递自定义的位置ThreadPoolExecutor(这是一个ExecutorService)。
所以回答你的问题:是的,线程池用于处理 I/O 完成,即使使用Asynchronous*NIO 类也是如此。
注意:一旦 Loom 项目完成并且 Fibers 占领了世界,这可能会改变。
| 归档时间: |
|
| 查看次数: |
481 次 |
| 最近记录: |