内核本身(无论是 Windows、Linux 还是其他更奇特的系统)负责执行非阻塞 I/O,而 nio 包中的 Java 类(例如 Channel 和 Selector)只是该 API 的相当低级别的翻译。
低级别的操作需要您创建线程才能正确执行。Java 中基本的 NIO 支持允许您调用一个方法,该方法会阻塞直到您感兴趣的任何数量的批量非阻塞通道中至少有一件事情发生。例如,您可以打开 1000 个表示网络套接字的通道,所有这些套接字都在等待“如果任何这些 1000 个打开的套接字上出现某些网络数据包,则我很感兴趣”,然后调用一个方法来说:“请休眠,直到发生有趣的事情”。如果您设置应用程序以调用此方法,然后处理所有有趣的事情,并返回调用此方法,则编写了一个效率相当低下的应用程序:CPU 倾向于拥有远远超过一个核心,除了一个核心外,所有核心都在睡觉什么也不做。正确的模型是拥有多个线程(或多或少每个核心一个)都运行相同的“唤醒我并给出有趣事物列表”的模型。除非您故意编写性能不佳的代码,否则无法摆脱线程。
因此,假设您已经设置好了:您拥有一个 8 核 CPU,并且您有 8 个线程运行“等待有趣的东西,处理具有活动数据的套接字”的循环。
想象一下,您的处理套接字代码的一部分被阻止。也就是说,它执行了某些操作,将导致 CPU 去检查其他要做的工作,因为它必须等待,比如等待网络或磁盘等。假设是因为您在其中放置了一些数据库查询,并且您没有意识到数据库查询使用了(可能是本地的,但仍然是)网络和磁盘。那将是非常糟糕的情况:您有充足的 CPU 资源来处理这 1000 个传入的请求,但是您的所有 8 个线程都在等待 DB 做事情,而虽然 CPU 可以分析数据包和响应,但它完全没有剩余的任务可做,因此会减速等待 DB 从磁盘中提取记录所需的时间。
很糟糕。因此,请
不要调用阻塞代码。不幸的是,Java 中有大量的方法(无论是在 Java 核心库还是第三方库中)都会阻塞。它们往往没有记录。这个问题没有真正的解决办法。
一些库确实提供了解决方案,但如果有的话,它必须是“回调”形式:以DB查询示例为例,你需要做的是将该网络套接字告诉它,你现在至少不再对传入数据感兴趣(你已经在等待DB响应,尝试处理此套接字的更多传入数据没有意义);相反,你想要将DB连接本身关联为“如果此DB查询有响应准备好,我就感兴趣”。Java作为一种语言并不适合以这种方式编写,你最终会陷入“回调地狱”的境地,这就是JavaScript的工作方式。虽然有解决回调地狱的方案,但仍然很复杂,而且Java基本上不支持它们(例如,“yield”是一个可以帮助的东西,但Java不支持yield概念)。
最后,还有性能问题:
为什么要摆脱线程?
线程有两个主要的惩罚:
- 上下文切换。当CPU必须跳到另一个线程时(因为它所在的线程需要等待磁盘或网络数据,因此现在无事可做),它需要跳到另一个代码位置,并确定加载到缓存中的内存表。
- 堆栈。就像几乎每个编程模型一样,有一个名为“堆栈”的内存位,其中包含局部变量和调用你的方法的位置(以及调用它的方法,一直到你的主要方法/线程运行方法)。如果你得到了一个堆栈跟踪,你正在看它的影响。在Java中,每个线程都有1个堆栈,所有堆栈的大小都相同。你可以使用
-Xss
JVM参数进行配置,最小值为1MB。这意味着,如果你想同时拥有4000个线程,那么需要4GB的堆栈,这是无法避免的(然后你需要更多的内存来处理堆等)。
但是,非阻塞并不能很好地解决这些问题:
移动到另一个处理程序时,因为您已经用完要处理的数据,所以您需要进行上下文切换。这不是线程切换,但是您仍然需要跳转到完全不同的内存页面,而在现代架构中,访问不在缓存中的内存部分需要很长时间。您只是将“线程上下文切换”换成了“内存页面缓存上下文切换”,并且没有获得任何好处。
假设您是某种聊天应用程序,并且从其中一个连接的客户端接收要发送的消息。现在,您需要查询数据库,以查看此用户是否有权将此消息发布到其打算发送到的聊天频道,并且还要查看是否有其他关注模式设备需要更新。因为这是阻止操作,您希望在等待时跳转到另一个作业。但是,您需要在某个地方记住此状态:发送用户、消息、DB查询结果。在线程模型中,此数据会自动且隐含地由堆栈空间处理:如果您采用全NIO,您需要自行管理此过程,例如使用ByteBuffers。
是的,当您手动控制字节缓冲区时,您可以使它们恰好达到所需大小,通常远小于1MB,因此您可以通过这种方式处理更多的同时连接,或者您只需在服务器中添加一个64GB的内存条。
因此,实际结果如下:
1. NIO代码极难编写。使用抽象化工具,例如Grizzly或Netty,因为这是“火箭科学”。
2. 它很少更快。
3. 如果需要跟踪连接/文件/作业等数量的数据很低,则可以同时进行更多操作。
4. 这有点像使用汇编语言而不是C语言,因为您可以在手动执行垃圾收集而不是让Java为您执行时从理论上挤出更好的性能。但是,大多数人不使用汇编语言来编写程序,即使它在理论上更快。绝大多数Web应用程序都是使用高级语言编写的,例如Java、Python、Node.js或其他一些高级内容,而不是像C(++)或汇编语言这样的非托管语言。
Executor
,您可以覆盖它。执行器运行它们自己的线程池。请参见java.nio.channels.AsynchronousChannelGroup
。但是,这仅用于调度完成。实际的异步I/O发生在内核中。 - user207421