`

Java NIO类库Selector机制解析

阅读更多

一、   前言

 

自从 J2SE 1.4 版本以来, JDK 发布了全新的 I/O 类库,简称 NIO ,其不但引入了全新的高效的 I/O 机制,同时,也引入了多路复用的异步模式。 NIO 的包中主要包含了这样几种抽象数据类型:

 

  • Buffer :包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的 I/O 操作。
  • Charset :它提供 Unicode 字符串影射到字节序列以及逆映射的操作。
  • Channels :包含 socket file pipe 三种管道,都是全双工的通道。
  • Selector :多个异步 I/O 操作集中到一个或多个线程中(可以被看成是 Unix select() 函数的面向对象版本)。

 

我在使用 NIO 类库书写相关网络程序的时候,发现了一些 Java 异常 RuntimeException ,异常的报错信息让他开始了对 NIO Selector 进行了一些调查。当ta对我共享了 Selector 的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。

 

二、   故事开始 : C++程序员写 Java程序 !

 

没有严重内存问题,大量丰富的 SDK 类库,超容易的跨平台,除了在性能上有些微辞, C++ 出身的程序员从来都不会觉得 Java 是一件很困难的事情。当然,对于长期习惯于使用操作系统 API (系统调用 System Call )的 C/C++ 程序来说,面对 Java 中的比较“另类”地操作系统资源的方法可能会略感困惑,但万变不离其宗,只需要对面向对象的设计模式有一定的了解,用不了多长时间, Java SDK 类库也能玩得随心所欲。

 

在使用 Java 进行相关网络程序的的设计时,出身 C/C++ 的人,首先想到的框架就是多路复用,想到多路复用, Unix/Linux 下马上就能让从想到 select, poll, epoll 系统调用。于是,在看到 Java NIO 中的 Selector 类时必然会倍感亲切。稍加查阅一下 SDK 手册以及相关例程,不一会儿,一个多路复用的框架便呈现出来,随手做个单元测试,没啥问题,一切和 C/C++ 照旧。然后告诉兄弟们,框架搞定,以后咱们就在 Windows 上开发及单元测试,完成后到运行环境 Unix 上集成测试。心中并暗自念到,跨平台就好啊,开发活动都可以跨平台了。

 

然而,好景不长,随着代码越来越多,逻辑越来越复杂。好好的框架居然在 Windows 上单元测试运行开始出现异常,看着 Java 运行异常出错的函数栈,异常居然由 Selector.open() 抛出,错误信息居然是 Unable to establish loopback connection

 

“Selector.open() 居然报 loopback connection 错误,凭什么?不应该啊? open 的时候又没有什么 loopback socket 连接,怎么会报这个错?

 

长期使用 C/C++ 的程序当然会对操作系统的调用非常熟悉,虽然 Java 的虚拟机搞的什么系统调用都不见了,但 C/C++ 的程序员必然要比 Java 程序敏感许多。

 

三、   开始调查 : 怎么 Java这么“傻” !

 

于是, C/C++ 的老鸟从 SystemInternals 上下载 Process Explorer 来查看一下究竟是什么个 Loopback Connection 果然,打开 java 运行进程,发现有一些自己连接自己的 localhost TCP/IP 链接。于是另一个问题又出现了,

 

凭什么啊?为什么会有自己和自己的连接?我程序里没有自己连接自己啊,怎么可能会有这样的链接啊?而自己连接自己的端口号居然是些奇怪的端口。

 

问题变得越来越蹊跷了。难道这都是 Selector.open() 在做怪?难道 Selector.open() 要创建一个自己连接自己的链接?写个程序看看:

 

import java.nio.channels.Selector;

import java.lang.RuntimeException;

import java.lang.Thread;

public class TestSelector {

    private static final int MAXSIZE= 5 ;

    public static final void main( String argc[] ) {

        Selector [] sels = new Selector[ MAXSIZE];

 

            try {

                 for ( int i = 0 ;i< MAXSIZE ;++i ) {

                    sels[i] = Selector.open();

                    //sels[i].close();

                }

                Thread.sleep( 30000 );

            } catch ( Exception ex ){

                throw new RuntimeException( ex );

            }

    }

}

 

这个程序什么也没有,就是做 5 Selector.open() ,然后休息 30 秒,以便我使用 Process Explorer 工具来查看进程。程序编译没有问题,运行起来,在 Process Explorer 中看到下面的对话框:(居然有 10 个连接,从连接端口我们可以知道,互相连接, 如:第一个连第二个,第二个又连第一个

 

 

 

 

不由得赞叹我们的 Java 啊,先不说这是不是一件愚蠢的事。至少可以肯定的是, Java 在消耗宝贵的系统资源方面,已经可以赶的上某些蠕虫病毒了。

 

如果不信,不妨把上面程序中的那个 MAXSIZE 的值改成 65535 试试,不一会你就会发现你的程序有这样的错误了:(在我的 XP 机器上大约运行到 2000 Selector.open() 左右)

 

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Unable to establish loopback connection

        at Test.main(Test.java:18)

Caused by: java.io.IOException: Unable to establish loopback connection

        at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)

        at java.security.AccessController.doPrivileged(Native Method)

        at sun.nio.ch.PipeImpl.<init>(Unknown Source)

        at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)

        at java.nio.channels.Pipe.open(Unknown Source)

        at sun.nio.ch.WindowsSelectorImpl.<init>(Unknown Source)

        at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)

        at java.nio.channels.Selector.open(Unknown Source)

        at Test.main(Test.java:15)

Caused by: java.net.SocketException: No buffer space available (maximum connections reached?): connect

        at sun.nio.ch.Net.connect(Native Method)

        at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)

        at java.nio.channels.SocketChannel.open(Unknown Source)

        ... 9 more

 

 

四、   继续调查 : 如此跨平台

 

当然,没人像我们这么变态写出那么多的 Selector.open() ,但这正好可以让我们来明白 Java 背着大家在干什么事。上面的那些“愚蠢连接”是在 Windows 平台上,如果不出意外, Unix/Linux 下应该也差不多吧。

 

于是我们把上面的程序放在 Linux 下跑了跑。使用 netstat 命令,并没有看到自己和自己的 Socket 连接。貌似在 Linux 上使用了和 Windows 不一样的机制?!

 

如果在 Linux 上不建自己和自己的 TCP 连接的话,那么文件描述符和端口都会被省下来了,是不是也就是说我们调用 65535 Selector.open() 的话,应该不会出现异常了。

 

可惜,在实现运行过程序当中,还是一样报错:(大约在 400 Selector.open() 左右,还不如 Windows

 

Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Too many open files

        at Test1.main(Test1.java:19)

Caused by: java.io.IOException: Too many open files

        at sun.nio.ch.IOUtil.initPipe(Native Method)

        at sun.nio.ch.EPollSelectorImpl.<init>(EPollSelectorImpl.java:49)

        at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)

         at java.nio.channels.Selector.open(Selector.java:209)

        at Test1.main(Test1.java:15)

 

我们发现,这个异常错误是 “Too many open files” ,于是我想到了使用 lsof 命令来查看一下打开的文件。

 

看到了有一些 pipe 文件,一共 5 对, 10 个(当然,管道从来都是成对的)。如下图所示。

 

 

 

 

可见, Selector.open() Linux 下不用 TCP 连接,而是用 pipe 管道。看来,这个 pipe 管道也是自己给自己的。所以,我们可以得出下面的结论:

 

1) Windows 下, Selector.open() 会自己和自己建立两条 TCP 链接。不但消耗了两个 TCP 连接和端口,同时也消耗了文件描述符。

2) Linux 下, Selector.open() 会自己和自己建两条管道。同样消耗了两个系统的文件描述符。

 

估计,在 Windows 下, Sun JVM 之所以选择 TCP 连接,而不是 Pipe ,要么是因为性能的问题,要么是因为资源的问题。可能, Windows 下的管道的性能要慢于 TCP 链接,也有可能是 Windows 下的管道所消耗的资源会比 TCP 链接多。这些实现的细节还有待于更为深层次的挖掘。

 

但我们至少可以了解,原来 Java Selector 在不同平台上的机制。

 

五、   迷惑不解 : 为什么要自己消耗资源?

 

令人不解的是为什么我们的 Java New I/O 要设计成这个样子?如果说老的 I/O 不能多路复用,如下图所示,要开 N 多的线程去挨个侦听每一个 Channel ( 文件描述符 ) ,如果这样做很费资源,且效率不高的话。那为什么在新的 I/O 机制依然需要自己连接自己,而且,还是重复连接,消耗双倍的资源?

 

通过 WEB 搜索引擎没有找到为什么。只看到 N 多的人在报 BUG ,但 SUN 却没有任何解释。

 

下面一个图展示了,老的 IO 和新 IO 的在网络编程方面的差别。看起来 NIO 的确很好很强大。但似乎比起 C/C++ 来说, Java 的这种实现会有一些不必要的开销。

 

 

 

六、   它山之石 : Apache Mina框架了解 Selector

 

上面的调查没过多长时间,正好同事也在开发网络程序,使用了 Apache Mina 框架。当我们把 Mina 框架的源码研读了一下后。发现在 Mina 中有这么一个机制:

 

1) Mina 框架会创建一个 Work 对象的线程。

2) Work 对象的线程的 run() 方法会从一个队列中拿出一堆 Channel ,然后使用 Selector.select() 方法来侦听是否有数据可以读 / 写。

3) 最关键的是,在 select 的时候,如果队列有新的 Channel 加入,那么, Selector.select() 会被唤醒,然后重新 select 最新的 Channel 集合。

4) 要唤醒 select 方法,只需要调用 Selector wakeup() 方法。

 

对于熟悉于系统调用的 C/C++ 程序员来说,一个阻塞在 select 上的线程有以下三种方式可以被唤醒:

1)   有数据可读 / 写,或出现异常。

2)   阻塞时间到,即 time out

3)   收到一个 non-block 的信号。可由 kill pthread_kill 发出。

所以, Selector.wakeup() 要唤醒阻塞的 select ,那么也只能通过这三种方法,其中:

 

1) 第二种方法可以排除,因为 select 一旦阻塞,应无法修改其 time out 时间。

2) 而第三种看来只能在 Linux 上实现, Windows 上没有这种信号通知的机制。

 

所以,看来只有第一种方法了。再回想到为什么每个 Selector.open() ,在 Windows 会建立一对自己和自己的 loopback TCP 连接;在 Linux 上会开一对 pipe pipe Linux 下一般都是成对打开),估计我们能够猜得出来——那就是如果想要唤醒 select ,只需要朝着自己的这个loopback 连接发点数据过去,于是,就可以唤醒阻塞在select上的 线程了。

 

七、   真相大白 : 可爱的 Java你太不容易了

 

使用 Linux 下的 strace 命令,我们可以方便地证明这一点。参看下图。图中,请注意下面几点:

1)   26654 是主线程,之前我输出 notify the select 字符串是为了做一个标记,而不至于迷失在大量的 strace log 中。

2)   26662 是侦听线程,也就是 select 阻塞的线程。

3)   图中选中的两行。 26654 write 正是 wakeup() 方法的系统调用,而紧接着的就是 26662 epoll_wait 的返回。

 

 

 

从上图可见,这和我们之前的猜想正好一样。可见, JDK Selector 自己和自己建的那些 TCP 连接或是 pipe ,正是用来实现 Selector notify wakeup 的功能的。

 

这两个方法完全是来模仿 Linux 中的的 kill pthread_kill 给阻塞在 select 上的线程发信号的。但因为发信号这个东西并不是一个跨平台的标准( pthread_kill 这个系统调用也不是所有 Unix/Linux 都支持的),而 pipe 是所有的 Unix/Linux 所支持的,但 Windows 又不支持,所以, Windows 用了 TCP 连接来实现这个事。

 

关于 Windows ,我一直在想, Windows 的防火墙的设置是不是会让 Java 的类似的程序执行异常呢?呵呵。如果不知道 Java SDK 有这样的机制,谁知道会有多少个程序为此引起的问题度过多少个不眠之夜,尤其是 Java 程序员。

分享到:
评论

相关推荐

    Java_NIO类库Selector机制解析.doc

    Java_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.doc

    Java-NIO类库Selector机制解析.docx

    "Java NIO Selector 机制解析" ...了解Java NIO类库和Selector机制可以帮助程序员更好地使用Java NIO类库,提高I/O操作的性能。但需要注意到Java虚拟机对操作系统调用的影响,避免出现异常和错误。

    Java2 类库参考手册

    4. **NIO(非阻塞I/O)**:`java.nio`包引入了新的I/O模型,允许异步读写,提高了大规模并发应用的性能。关键组件包括`Channel`、`Buffer`和`Selector`。 5. **多线程**:`java.lang.Thread`和`java.util....

    tiny-web-server:使用 java nio 构建的原型 Web 服务器。 灵感来自 Netty

    【标签】"Java"明确了这个项目是用Java语言编写的,Java作为一门广泛应用的编程语言,其丰富的类库和强大的跨平台能力使得开发网络服务器成为可能。 至于【压缩包子文件的文件名称列表】中的“tiny-web-server-...

    深度解析java游戏服务器开发.zip

    Java以其跨平台的特性、丰富的类库以及强大的性能,成为了后端开发的首选语言之一,尤其适合大型、复杂的游戏服务器开发。 1. **网络编程**:游戏服务器的核心是处理客户端的连接请求和数据传输。Java的Socket编程...

    Servlet API 和 NIO: 最终组合在一起

    解决这些问题通常需要深入理解NIO的工作原理,包括缓冲区(Buffer)、通道(Channel)、选择器(Selector)等核心概念,并且需要对Servlet容器的内部机制有一定的了解。 在提供的文件列表中,"src"目录可能包含了源...

    1_JAVA核心知识点整理.pdf

    - Java NIO还包括缓冲区(Buffer)、通道(Channel)、选择器(Selector)等概念。 5. JVM类加载机制 - 类加载过程包括加载、验证、准备、解析、初始化等步骤。 - 类加载器分为启动类加载器(BootstrapClassLoader)、...

    jdk sun 开头的源码

    1. **NIO(Non-blocking I/O)**:Java的非阻塞I/O模型,提供了通道(Channel)和选择器(Selector)等概念,极大地提高了处理大量并发连接的能力。在`sun.nio`包下,你可以找到`sun.nio.ch`子包,它包含了Java NIO...

    jdk 源码 保护 sun com.sum nio misc 等 rc.jar 中的源码

    在`java.nio`包中,`FileChannel`、`SocketChannel`和`Selector`等类是核心部分,通过查看其源码,开发者可以学习到如何高效地进行多路复用I/O,以及如何利用非阻塞特性来优化网络通信。 最后,`misc`(杂项)一词...

    java面试 java书籍

    Java是一种广泛使用的面向对象的编程语言,以其跨平台、高性能和丰富的类库而备受赞誉。在IT行业,尤其是软件开发领域,Java工程师是需求量极大的职位。为了在Java面试中脱颖而出,深入理解Java语言的核心概念和技术...

    JAVA API1.6中文文档

    4. **I/O与NIO**:Java API 1.6不仅有传统的基于流的I/O,还引入了非阻塞I/O(New IO,即NIO)框架,提供了一种更高效的数据传输方式,包括`java.nio`包中的通道(Channel)、缓冲区(Buffer)和选择器(Selector)...

    JAVA核心知识点整理.pdf

    Java IO/NIO部分介绍了Java中的输入输出处理,包括不同的IO模型(阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO),以及Java NIO包中Buffer、Channel、Selector等重要组件。NIO通过使用缓冲区作为数据临时存储...

    java 开发工具 jdk 1.4 免安装版

    JDK(Java Development Kit)是Oracle公司提供的用于开发Java应用程序的重要软件包,它包含了Java编译器、Java虚拟机(JVM)、Java类库以及各种开发和调试工具,是Java开发的基础。 JDK 1.4版本是Java历史上的一个...

    java_API16

    2. **IO流**:Java 1.6对输入/输出流进行了优化,提供了NIO(New Input/Output)框架,它支持选择器(Selector)和通道(Channel)等特性,提高了IO操作的性能和并发性。 3. **网络编程**:Java API 1.6提供Socket...

    Java网络高级编程源码人邮金勇华曲俊生

    3. **NIO(非阻塞I/O)**:Java NIO(New Input/Output)是Java 1.4引入的新特性,它提供了与传统IO不同的I/O操作方式,支持选择器(Selector)、通道(Channel)和缓冲区(Buffer)。NIO允许程序在数据准备好时进行...

    java网络教程

    相对于传统的IO模型,NIO提供了选择器(Selector)和通道(Channel)等机制,可以实现多路复用,提高网络通信的效率,尤其适合高并发场景。 通过这个Java网络教程,学习者将能够熟练地运用Java进行网络编程,包括...

    java io

    NIO提供了一种非阻塞I/O模型,通过选择器(Selector)监控多个通道(Channel),实现同时处理多个输入输出任务。通道是NIO的核心概念,如FileChannel、SocketChannel等,它们可以从或向缓冲区(Buffer)读写数据。...

    javaAPI

    5. **java.nio**:非阻塞I/O,提供了Channel、Buffer和Selector等,提高了I/O性能。 6. **java.awt**和**javax.swing**:这两个包用于创建图形用户界面,其中java.awt提供基本组件,javax.swing提供了更为高级和...

    Java网络编程实例

    `java.nio`包提供了选择器(Selector)、通道(Channel)和缓冲区(Buffer)等组件,可以实现高并发和低延迟的网络服务。AIO(Java NIO.2)引入了`AsynchronousServerSocketChannel`和`AsynchronousSocketChannel`,...

Global site tag (gtag.js) - Google Analytics