Java nio从jdk1.4开始引入进来的。通常听到比较多的说法是: 你还没有用nio啊,你“out”啦;nio不阻塞,传统的I/O都是阻塞的,效率很低...... 如果你的系统在使用传统I/O上已经工作得很好且没有更多的性能等方面的要求,千万不要盲目地使用nio来重构(也许在你的系统的上下文环境中,使用nio所带来其它方面的复杂性会提高,处理增多,整体效率并未提高),这至少不符合敏捷开发的态度,简单说,不要为了nio而nio。当然这不阻碍我们对nio的认识。
Java nio是在Java编程中对I/O的另外一种高级的抽象方式。其对I/O的使用方式也更加贴近操作系统使用I/O的方式:通道和字节缓冲。一般的用法可以参考<<Thinking in java>>。这里我们谈谈使用nio进行网络通信编程的特点。基本的Java socket编程对小规模的系统可以很好的工作,但是在资源不扩展而用户请求不断增大的情况下,系统劣化的比率很大。我们通常会面临这样一些这些问题:
1、thread-per-request 的方式,这样会有更多线程维护和切换的系统开销,同时在系统扩展性方面受到限制。
2、针对问题1,我们可以采用线程池的方式来节省线程创建、维护以及切换的开销,但是这样也会限制系统可以同时处理客户端的数量,至少对一些长连接协议来说是这样。此时增加线程池的大小是不能再提高系统性能的,而只会增加系统更多的线程开销。
3、传统的socket在读/写数据以及建立连接上都是阻塞式的(没有数据可读可能会阻塞;没有足够空间缓存传输的数据可能阻塞;服务端的accept方法以及socket构造函数都会阻塞等待,直到连接建立) 。这些特性会降低整个系统对CPU的利用率以及系统的活跃性,降低系统对客户请求的响应性及响应时间,在某些情况下是不可接受的甚至会带来灾难性的结果。在这样的情形下,我们要解决cpu利用率、线程活跃性以及提高系统吞吐量,我们势必做很多额外的工作且相当复杂。
4、由于传统socket的阻塞特性,每个对等端都在阻塞等待另外一端完成相关处理,这样势必增大死锁的风险。
5、当然为了提高系统的活跃性,传统的socket在accept、构造socket以及read方法上都增加了超时控制的处理以及使用socket.close方法来打断一些阻塞操作,而网络总是不确定的,设置这些超时时间的设置在具体的上下文环境中的取值也是比较复杂的。简而言之,为了提高系统的活跃性会增大系统的复杂度。
那么,Java nio真的就是Java socket编程解决以上若干问题的灵丹妙药吗?答案是否定的。
nio的非阻塞特性是nio最大的特点,所谓非阻塞无非就是将通信的信道设置成为非阻塞的情况下,对该信道的所有操作都是会立即返回的。如数据读取,没有数据的时候会返回0而不是阻塞在信道上。这样就增大了系统的活跃性,使得系统不必浪费资源的I/O操作上,系统可以更好的利用cpu资源做一些其它处理,在某些场景下会提高系统的响应性以及吞吐量。
同时nio在网络通信的中采用的网络I/O事件驱动的方式,即操作系统对用户感兴趣的通信信道及其上面的I/O事件进行监听并通知应用程序。这实际上也是观察者模式的一种应用,在这样的背景下,问题4中的死锁风险几乎就没有了。
如果你开发的服务端不是迭代服务器(顺序化处理每个客户请求),那么对于问题1、2在nio服务器中也是一样的。换句话说,我们开发的并发处理服务器为了实现对各种网络I/O事件的处理,并对每种我们所关心的网络I/O事件进行及时响应,采用thread-per-request或者线程池的方式是无法避免的。所以很多人会感觉nio的网络编程结构与非nio的网络编程结构实际是一样的,accept换成了select,监听连接入站编程了监听信道上感兴趣的I/O事件,为了提高系统的处理能力,还是要启动异步的线程来处理信道上的网络I/O事件,只是具体实现不一样而已。这说明nio并没有改变我们服务端程序编写的整体结构,只是在nio的环境下,我们的确可以去提高系统的活跃性、响应性和吞吐量。nio与非nio在读/写网络数据以及连接建立等网络操作上是没有多大区别的,这些因素主要都还是取决于网络状况、操作系统协议栈实现、应用自身处理网络数据的方式等多个方面。甚至,我们在享用nio的同时在一定程度上还会增大了网络编程的复杂度,因为数据的读写以及连接的建立等操作变得更加不确定,当然最终网络编程的复杂还是取决于协议的复杂度。