一、 前言
自从 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 进行了一些调查。当赵锟对我共享了 Selector 的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在伙同赵锟进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。这也是为什么本文的作者署名为我们两人的原因。
先要说明的一点是,赵锟和我本质上都是出身于 Unix/Linux/C/C++ 的开发人员,对于 Java ,这并不是我们的长处,这篇文章本质上出于对 Java 的 Selector 的好奇,因为从表面上来看 Selector 似乎做到了一些让我们这些 C/C++ 出身的人比较惊奇的事情。
下面让我来为你讲述一下这段故事。
二、 故事开始 : 让 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 );
}
}
}
#p# #e#
这个程序什么也没有,就是做 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.
(Unknown Source)
at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)
at java.nio.channels.Pipe.open(Unknown Source)
at sun.nio.ch.WindowsSelectorImpl.(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.(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 个(当然,管道从来都是成对的)。如下图所示。
#p# #e#
可见, 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程序员。
来源网络
- 大小: 46.9 KB
- 大小: 32 KB
- 大小: 65.2 KB
分享到:
相关推荐
Java_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.doc
"Java NIO Selector 机制解析" Java NIO(New I/O)类库是Java 1.4版本以后引入的新一代I/O机制,相比传统的I/O机制,NIO提供了高效、异步、多路复用的I/O操作模式。Selector机制是NIO类库中的一种核心机制,用于...
Java NIO,全称为Non-blocking Input/Output,是Java在1.4版本引入的一个新特性,旨在提供一种更高效、更灵活的I/O操作...结合实际项目需求,合理运用Java NIO的Selector机制,可以显著提升系统的吞吐量和响应速度。
JavaNIO库Selector机制解析.docx
- **创建Selector**:首先,我们需要通过`java.nio.Selector.open()`方法创建一个Selector实例。 - **注册Channel**:然后,我们可以将ServerSocketChannel、SocketChannel或其他类型的通道注册到Selector上,指定...
java侧起server(NioUdpServer1.java),基于Java Nio的selector 阻塞等候,一个android app(NioUdpClient1文件夹)和一个java程序(UI.java)作为两个client分别向该server发数据,server收到后分别打印收到的消息...
Java NIO(New IO)是Java 1.4版本引入的一个新特性,它提供了一种新的方式来处理I/O操作,相比传统的IO模型,NIO具有更好的性能和更高的效率。Selector是Java NIO中的核心组件之一,它允许单个线程处理多个通道...
Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道之间的数据传输 Java NIO系列教程(六) Selector Java NIO系列教程(七) FileChannel Java NIO系列教程(八) ...
14-Java NIO-Buffer-三个属性和类型.mp4 17-Java NIO-Buffer-缓冲区分片.mp4 18-Java NIO-Buffer-只读缓冲区.mp4 19-Java NIO-Buffer-直接缓冲区.mp4 21-Java NIO-Selector-概述.mp4 23-Java NIO-Selector-示例代码...
java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java...
除此之外,Java还提供了其他类库,如`java.text`用于文本格式化,`java.sql`用于数据库连接,`java.awt`和`javax.swing`用于图形用户界面(GUI)编程,以及`java.nio`包提供的非阻塞I/O操作。 总的来说,Java常用...
Java NIO的核心组件包括Channel(通道)、Buffer(缓冲区)和Selector(选择器)。 - **Channel**:通道用于连接源和目的地,支持数据的读写操作。与传统的流不同,通道支持双向通信,并且可以通过非阻塞的方式进行...
此外,`java.xml`和`javax.xml`包提供了XML处理的工具,如解析、转换和验证,这对于数据交换和配置文件管理至关重要。 数据库连接方面,`java.sql`包提供了与关系型数据库交互的API,包括`Connection`、`Statement`...
Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java平台中用于替代标准I/O(BIO)模型的一种新机制。NIO在Java 1.4版本引入,提供了更高效的数据处理和通道通信方式,特别适用于高并发、大数据...
Java NIO(New Input/Output)是Java标准库中提供的一种I/O模型,与传统的 Blocking I/O(同步阻塞I/O)相对。NIO在Java 1.4版本引入,其设计目标是提供一种更高效、更灵活的I/O操作方式,特别适合处理大量并发连接...
Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java标准库提供的一种替代传统的I/O模型的新技术。自Java 1.4版本引入NIO后,它为Java开发者提供了更高效的数据传输方式,尤其是在处理大量并发...
总的来说,这个项目展示了如何使用Java NIO的`Selector`机制来实现一个高并发的聊天应用,同时还包含了图形用户界面的设计,使得用户能够直观地参与聊天。通过学习和理解这个项目,开发者可以深入掌握Java NIO的使用...
在这个文档中,你可以找到关于Java NIO类库的详细说明,例如`FileChannel`用于文件操作,`SocketChannel`和`ServerSocketChannel`用于网络通信,`ByteBuffer`作为最常用的缓冲区类型,以及`Selector`的使用方法等。...
总结来说,Java NIO通过非阻塞I/O和选择器机制,提供了更高效、灵活的I/O处理能力,降低了系统资源消耗,尤其适合于处理高并发的网络通信和文件操作。随着Java版本的更新,NIO的功能也在不断完善,如NIO 2引入了异步...