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进行了一些调查。当赵锟对我共享了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 );
}
}
}
这个程序什么也没有,就是做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引出来的其它话题还有许多,比如关于GNU 的Java编译器又是如何,它是否会像Sun的Java解释器如此做傻事?我在这里先卖一个关子,关于GNU的Java编译器,我会在另外一篇文章中讲述,近期发布,敬请期待。
关于本文中所使用的实验平台如下:
- · Windows:Windows XP + SP2, Sun J2SE (build 1.7.0-ea-b23)
- · Linux:Ubuntu 7.10 + Linux Kernel 2.6.22-14-generic, J2SE (build 1.6.0_03-b05)
来源:
http://blog.csdn.net/haoel/article/details/2224069
http://blog.csdn.net/haoel/article/details/2224055
Java NIO类库Selector机制解析--转的更多相关文章
- Java NIO类库Selector机制解析(上)
一. 前言 自从J2SE 1.4版本以来,JDK发布了全新的I/O类库,简称NIO,其不但引入了全新的高效的I/O机制,同时,也引入了多路复用的异步模式.NIO的包中主要包含了这样几种抽象数据类型: ...
- Java NIO类库Selector机制解析(下)
五. 迷惑不解 : 为什么要自己消耗资源? 令人不解的是为什么我们的Java的New I/O要设计成这个样子?如果说老的I/O不能多路复用,如下图所示,要开N多的线程去挨个侦听每一个Channel ...
- Java NIO 选择器(Selector)的内部实现(poll epoll)
http://blog.csdn.net/hsuxu/article/details/9876983 之前强调这么多关于linux内核的poll及epoll,无非是想让大家先有个认识: Java NI ...
- Java NIO之Selector(选择器)
历史回顾: Java NIO 概览 Java NIO 之 Buffer(缓冲区) Java NIO 之 Channel(通道) 其他高赞文章: 面试中关于Redis的问题看这篇就够了 一文轻松搞懂re ...
- Java NIO 选择器(Selector)的内部实现(poll epoll)(转)
转自:http://blog.csdn.net/hsuxu/article/details/9876983 之前强调这么多关于linux内核的poll及epoll,无非是想让大家先有个认识: Java ...
- Java NIO之Selector
选择器是JavaNIO重磅推出的一个概念:在旧有的系统中为了跟踪多端口消息,需要为每一个端口配备一个线程做监听:但是有了selector就不需要了,一个Selector可以管理一众渠道(channel ...
- Netty快速入门(05)Java NIO 介绍-Selector
Java NIO Selector Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读.可写.如此可以实现单线程管理多个channels,也就是 ...
- JAVA NIO 类库的异步通信框架netty和mina
Netty 和 Mina 我究竟该选择哪个? 根据我的经验,无论选择哪个,都是个正确的选择.两者各有千秋,Netty 在内存管理方面更胜一筹,综合性能也更优.但是,API 变更的管理和兼容性做的不是太 ...
- JAVA NIO 之 Selector 组件
NIO 重要功能就是实现多路复用.Selector是SelectableChannel对象的多路复用器.一些基础知识: 选择器(Selector):选择器类管理着一个被注册的通道集合的信息和它们的就绪 ...
随机推荐
- JS中的call、apply、bind方法
JS中的call.apply.bind方法 一.call()和apply()方法 1.方法定义 call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]] ...
- WPF之Treeview控件简单用法
TreeView:表示显示在树结构中分层数据具有项目可展开和折叠的控件 TreeView 的内容是可以包含丰富内容的 TreeViewItem 控件,如 Button 和 Image 控件.TreeV ...
- 从文章"避免复制与粘贴"到文章"Extract Method"的反思(1)
看了一个比我牛的人的博客园的博文"避免复制和粘贴".里面提到了重构手法Extract Method. 所以又搜了一下Extract Method. 这里先自我理解Extract ...
- ASP.NET MVC轻教程 Step By Step 11——数据注解
将验证规则写在Cotroller里不是一个好办法,这样会显得代码很啰嗦,更重要的是将业务逻辑写入Controller,使得Controller变得更“重”,不符合设计原则.更好的办法是使用验证注解属性 ...
- Lodash,你正在使用的JavaScript库
JavaScript工具库lodash发布了3.5版,成为了npm包仓库中依赖最多的库.它正在摆脱屌丝身份,成为开发者的不二之选. lodash一开始是Underscore.js库的一个fork,因为 ...
- Delphi中代替WebBrowser控件的第三方控件
这几天,接触到在delphi中内嵌网页,用delphi7自带的TWebBrowser控件,显示的内容与本机IE8显示的不一样,但是跟装IE8之前的IE6显示一个效果.现在赶脚是下面两个原因中的一个: ...
- bzoj3166
首先不难想到穷举次大数然后我们只要找到满足这个数是次大数的最大区间即可显然答案只可能是这两种[LL[i]+1,R[i]-1]和[L[i]+1,RR[i]-1]L[i]表示这个数ai左侧第一个比它大的数 ...
- wpa_supplicant无线网络配置
目前可以使用wireless-tools 或wpa_supplicant工具来配置无线网络.请记住重要的一点是,对无线网络的配置是全局性的,而非针对具体的接口. wpa_supplicant是一个较好 ...
- Ajax的简单小例子
1.首先下载ajax.dll,一个百度一下都有下载的!自行查找. 2.把ajax.dll导入到工程.右键工程-->添加引用--->浏览,找到下载好的ajax.dll文件,点击确定,这时候在 ...
- 最大流算法---Edmond-Karp
这个算法是基于FF方法,就是通过不断求残余网络的增广路来增广流量,直到找不到增广路为止.注意:每次找到增广路以后都要更新原网络.EK算法通过BFS寻找源S到汇T的一条最短路径,因此时间复杂度是O(VE ...