目前JAVA中IO的方式分为:同步阻塞的BIO、同步非阻塞/轮询的NIO、异步非阻塞的AIO
Java 1.4(JSR 51)开始支持NIO
Java 1.7(JSR 203)引入了NIO.2和AIO
关于阻塞与非阻塞的区别在于,是对每一个套接字请求都启用一个新线程,并全程等待I/O的完成,才开始处理,处理完成后,等待I/O发送响应(阻塞)。还是用一个选择器注册一个套接字通道,当通道准备好可以开始I/O操作时发出通知(非阻塞)
关于同步异步的区别在于,是线程亲自负责观察I/O事件,处理和响应(同步)。还是把观察I/O事件,处理和响应委托给底层操作系统,线程只需注册相应事件和回调句柄,由操作系统完成后回调过来(异步)。异步I/O需要底层操作系统的支持才能实现。
一、 Reactor and Proactor
IO读写时,多路复用机制都会依赖对一个事件多路分离器,负责把源事件的IO 事件分离出来,分别到相应的read/write事件分离器。涉及到事件分离器的两种模式分别就是 Reactor和Proactor,Reactor是基于同步IO的,Proactor是基于异步IO的。
在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有 overlapped的技术),事件分离者等IOCompletion事件完成. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。
在Reactor中实现读:
- 注册读就绪事件和相应的事件处理器
- 事件分离器等待事件
- 事件到来,激活分离器,分离器调用事件对应的处理器。
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
与如下Proactor(真异步)中的读过程比较:
- 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
- 事件分离器等待操作完成事件
- 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
- 事件分离器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。
可以看出,两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操作可以进行或已经完成)。在结构
上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;
不同点在于,异步情况下(Proactor),当回调handler时,表示IO操作已经完成;同步情况下(Reactor),回调handler时,表示
IO设备可以进行某个操作(can read or can write),handler这个时候开始提交操作。
二、BIO、NIO、AIO
NIO通常采用Reactor模式,AIO通常采用Proactor模式。AIO简化了程序的编写,stream的读取和写入都有OS来完成,不需要像NIO那样子遍历Selector。Windows基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。
4种通信方式:TCP/IP+BIO, TCP/IP+NIO, UDP/IP+BIO, UDP/IP+NIO。
TCP/IP+BIO、
Socket和ServerSocket实现,ServerSocket实现Server端端口监听,Socket用于建立网络IO连接。
不适用于处理多个请求 1.生成Socket会消耗过多的本地资源。2. Socket连接的建立一般比较慢。
BIO情况下,能支持的连接数有限,一般都采取accept获取Socket以后采用一个thread来处理,one connection one thread。无论连接是否有真正数据请求,都需要独占一个thread。
可以通过设立Socket池来一定程度上解决问题,但是使用池需要注意的问题是:1. 竞争等待比较多。 2. 需要控制好超时时间。
TCP/IP+NIO
使用Channel(SocketChannel和ServerSocketChannel)和Selector。
Server端通常由一个thread来监听connect事件,另外多个thread来监听读写事件。这样做的好处是这些连接只有在真是请求的时候才会创建thread来处理,one request one thread。这种方式在server端需要支持大量连接但这些连接同时发送请求的峰值不会很多的时候十分有效。
UDP/IP+BIO
DatagramSocket和DatagramPacket。DatagramSocket负责监听端口以及读写数据,DatagramPacket作为数据流对象进行传输。
UDP/IP是无连接的,无法进行双向通信,除非双方都成为UDP Server。
UDP/IP+NIO
通过DatagramChannel和ByteBuffer实现。DatagramChannel负责端口监听及读写。ByteBuffer负责数据流传输。
如果要将消息发送到多台机器,如果为每个目标机器都建立一个连接的话,会有很大的网络流量压力。这时候可以使用基于UDP/IP的Multicast协议传输,Java中可以通过MulticastSocket和DatagramPacket来实现。
Multicast一般多用于多台机器的状态同步,比如JGroups。SRM, URGCP都是Multicast的实现方式。eBay就采用SRM来实现将数据从主数据库同步到各个搜索节点机器。
NIO由下列API组成:
◆ 原始类型数据缓冲
◆ 字符集编码和解码
◆ 通道,新的原始I/O抽象
◆ 支持上锁和内存映射的文件接口,文件最大支持Integer.MAX_VALUE字节(2GB)
◆ 为可扩展服务器提供的多路复用,无阻塞I/O设施(基于选择器和键)
JSR 203(NIO.2)除了解决JSR 51遗留下来的问题外,还为Java平台提供了更多新的I/O API,NIO.2解决了java.awt.File文件系统接口存在的重大问题,引入了异步I/O,并完成了未包括在JSR 51中的功能,下面列出了包含在JSR 203中的主要组件:
◆ 新的文件系统接口,支持大块访问文件属性,更改通知,绕开文件系统指定的API,也是可插拔文件系统实现的服务提供者接口。
◆ 对套接字和文件同时提供了异步I/O操作的API。
◆ JSR 51中定义的完整的套接字通道功能,此外还包括绑定,选项配置和多播数据报的支持。
新的文件系统接口
Java的File类存在重大问题,例如,操作出错时,delete()和mkdir()方法返回一个状态码而不是一个异常,没有办法获知失败的原因,此外还包括以下问题:
◆ File没有提供方法来检测符号链接,要知道为什么检测符号链接很重要,以及如何解决这个问题的办法,请参考Patrick的文章“在Java中如何处理文件系统软链接/符号链接”和“Java中的链接/别名/快捷方式”。
◆ File提供的方法只能访问部分文件属性,不能访问文件权限和访问控制列表。
◆ File没有提供方法一次访问文件的所有属性(如文件的修改时间和它的类型),因为文件系统需要为每个属性执行查询请求,可能存在性能问题。
◆ File的list()和listFiles()方法返回文件名和目录名的数组,但不支持大目录,通过网络展示大目录清单时,调用list()/listFiles()方法可能会使当前的线程阻塞相当长一段时间,而在服务器端,虚拟机可能会耗尽内存。
◆ File没有提供复制和移动文件的方法,虽然File提供了一个renameTo()方法在某些时候可以用来移动文件,但它的行为与平台关系紧密,即在不同平台上的行为是不一致的,根据renameTo()的文档说明,这个方法不能在文件系统之间移动文件,它可能不是原子的,如果目标路径下已存在同名文件,这个操作可能不会成功。
◆ File也没有提供改变通知方法,需要应用程序自己实现,因此导致应用程序的性能下降,例如,服务器需要确定什么时候往目录中添加了一个新的JAR文件,它需要实时监视这个目录,因为服务器后台线程需要频繁读取文件系统,因此性能会有所下降。
◆ File也不允许开发人员引入他们自己的文件系统访问功能,例如,开发人员可能想将文件系统存储到一个zip文件中,或创建一个内存文件系统。
NIO.2引入了新的文件系统接口,除了解决上述存在的问题外,还引入了更多的功能,这个接口由位于java.nio.file,java.nio.file.attribute和java.nio.file.spi包中的类和其它类型组成。
这些包提供了多个切入点,其中一个切入点就是java.nio.file.Paths类,它提供了两个方法返回一个java.nio.file.Path实例:
◆ public static Path get(String path) – 它通过转换给定路径字符串返回给这个实例构造一个Path实例。
◆ public static Path get(URI uri) -它通过转换给定路径的URI(统一资源定位符)返回给这个实例构造一个Path实例。
与传统的基于File的代码互操作:
File类提供了一个public Path toPath()方法,它可以将一个File实例转换成一个Path实例。
当你创建了一个Path实例后,你就可以使用这个实例执行许多路径操作(如返回路径的一部分,连接两个路径)和许多文件操作(如删除,移动和复制文件)。
为了不将问题复杂化,我就不深入讲解Path了,这里我用一段代码简单地演示一下以前的get()方法和Path的delete()方法。
清单1. InformedDelete.java
- // InformedDelete.java
- import java.io.IOException;
- import java.nio.file.DirectoryNotEmptyException;
- import java.nio.file.NoSuchFileException;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- public class InformedDelete
- {
- public static void main (String [] args)
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java InformedDelete path");
- return;
- }
- // Attempt to construct a Path instance by converting the path argument
- // string. If unsuccessful (you passed an empty string as the
- // command-line argument), the get() method throws an instance of the
- // unchecked java.nio.file.InvalidPathException class.
- Path path = Paths.get (args [0]);
- try
- {
- path.delete (); // Attempt to delete the path.
- }
- catch (NoSuchFileException e)
- {
- System.err.format ("%s: no such file or directory%n", path);
- }
- catch (DirectoryNotEmptyException e)
- {
- System.err.format ("%s: directory not empty%n", path);
- }
- catch (IOException e)
- {
- System.err.format ("%s: %s%n", path, e);
- }
- }
- }
InformedDelete调用Path的delete()方法解决了File的delete()方法不能确定失败原因的问题,当Path的delete()当的检测到操作失败时,它会根据情况抛出适当的异常,如:
◆ 如果文件不存在,抛出java.nio.file.NoSuchFileException异常。
◆ 如果文件是一个目录不能删除,抛出java.nio.file.DirectoryNotEmptyException异常,因为这个目录下可能还包括一个空目录。
◆ 如果遇到其他I/O问题,则抛出java.io.IOException的子类异常,例如,如果文件是只读的,抛出java.nio.file.AccessDeniedException异常。
异步I/O
JSR 51引入了多路复用I/O(无阻塞I/O和选择就绪的结合)使创建高可扩展服务器变得更加容易,本质上是这样的,客户端代码用一个选择器注册一个套接字通道,当通道准备好可以开始I/O操作时发出通知。
如果要深入研究多路复用I/O,请阅读Ron Hitchens的《Java NIO》一书。
JSR 203还引入了异步I/O,它也被用来建立高可扩展服务器,和多路复用I/O不同,异步I/O是让客户端启动一个I/O操作,当操作完成后向客户端发送一个通知。
异步I/O是通过以下位于java.nio.channels包中的接口和类实现的,它们的名称前面都加了Asynchronous前缀:
◆ AsynchronousChannel – 标识一个支持异步I/O的通道。
◆ AsynchronousByteChannel – 标识一个支持读写字节的异步通道,这个接口扩展了AsynchronousChannel。
◆ AsynchronousDatagramChannel – 标识一个面向数据报套接字异步通道,这个类实现了AsynchronousByteChannel。
◆ AsynchronousFileChannel – 标识一个可读,写和操作文件的异步通道,这个类实现了AsynchronousChannel。
◆ AsynchronousServerSocketChannel – 标识一个面向流监听套接字的异步通道,这个类实现了AsynchronousChannel。
◆ AsynchronousSocketChannel – 标识一个面向流连接套接字的异步通道,这个类实现了AsynchronousByteChannel。
◆ AsynchronousChannelGroup – 标识一个用于资源共享的异步通道组。
AsynchronousChannel文档指定了两种形式的异步I/O操作:
◆ Future operation(...)
◆ void operation(... A attachment, CompletionHandler handler)
operation列举I/O操作(如读,写),V是操作的结果类型,A是附加给操作的对象类型。
第一种形式需要你调用java.util.concurrent.Future方法检查操作是否完成,等待完成和检索结果,清单2的代码演示了这样一个示例。
清单2. AFCDemo1.java
- // AFCDemo1.java
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.channels.AsynchronousFileChannel;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.util.concurrent.Future;
- public class AFCDemo1
- {
- public static void main (String [] args) throws Exception
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java AFCDemo1 path");
- return;
- }
- Path path = Paths.get (args [0]);
- AsynchronousFileChannel ch = AsynchronousFileChannel.open (path);
- ByteBuffer buf = ByteBuffer.allocate (1024);
- Future<Integer> result = ch.read (buf, 0);
- while (!result.isDone ())
- {
- System.out.println ("Sleeping...");
- Thread.sleep (500);
- }
- System.out.println ("Finished = "+result.isDone ());
- System.out.println ("Bytes read = "+result.get ());
- ch.close ();
- }
- }
调用AsynchronousFileChannel's public static AsynchronousFileChannel open(Path file, OpenOption... options)方法打开file参数进行读取,然后创建了一个字节缓冲区存储读取操作的结果。
接下来调用public abstract Future read(ByteBuffer dst, long position)方法异步读取文件的前1024个字节,这个方法返回一个Future实例代表这个操作的结果。
调用read()方法后,进入一个表决循环,重复调用Future的isDone()方法检查操作是否完成,一直等到读操作结束,最后调用Future的get()方法返回读取到的字节大小。
第二种形式需要你指定java.nio.channels.CompletionHandler,并实现下面的方法使用前面操作返回的结果,或是了解操作为什么失败,并采取适当的行动:
◆ 当操作完成时调用void completed(V result, A attachment),这个操作的结果是由result标识的,附加给操作的对象是由attachment标识的。
◆ 当操作失败时调用void failed(Throwable exc, A attachment),操作失败的原因是由exc标识的,附加给操作的对象是由attachment标识的。
我创建了一个程序演示创建和接收读操作状态的通知,其代码如清单3所示。
清单3. AFCDemo2.java
- // AFCDemo2.java
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.nio.channels.AsynchronousFileChannel;
- import java.nio.channels.CompletionHandler;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- public class AFCDemo2
- {
- static Thread current;
- public static void main (String [] args) throws Exception
- {
- if (args.length != 1)
- {
- System.err.println ("usage: java AFCDemo1 path");
- return;
- }
- Path path = Paths.get (args [0]);
- AsynchronousFileChannel ch = AsynchronousFileChannel.open (path);
- ByteBuffer buf = ByteBuffer.allocate (1024);
- current = Thread.currentThread ();
- ch.read (buf, 0, null,
- new CompletionHandler<Integer, Void> ()
- {
- public void completed (Integer result, Void v)
- {
- System.out.println ("Bytes read = "+result);
- current.interrupt ();
- }
- public void failed (Throwable exc, Void v)
- {
- System.out.println ("Failure: "+exc.toString ());
- current.interrupt ();
- }
- });
- System.out.println ("Waiting for completion");
- try
- {
- current.join ();
- }
- catch (InterruptedException e)
- {
- }
- System.out.println ("Terminating");
- ch.close ();
- }
- }
上面的代码调用AsynchronousFileChannel's public abstract void read(ByteBuffer dst, long position, A attachment, CompletionHandler handler)方法异步读取前1024字节。
虽然我们只演示了单一的读操作,但attachment部分也很重要,上面的代码演示了传递一个null给read()方法,并指定附加类型为Void。
完整的套接字通道功能
JSR 51的DatagramChannel,ServerSocketChannel和SocketChannel类没有完整抽象一个网络套接字,为了绑定通道的套接字,或为了获得/设置套接字选项,你必须先调用每个类的socket()方法检索对等套接字。
JSR 51生效时没有时间定义完整的套接字通道API,因此形成了套接字通道和套接字API混合的局面,JSR203引入新的java.nio.channels.NetworkChannel接口解决了这个问题。
NetworkChannel提供了将套接字绑定到本地地址,返回绑定地址,以及获得/设置套接字选项的方法,这个接口是通过同步和异步套接字类实现的,不再需要调用socket()方法。
JSR 203也引入了新的java.nio.channels.MulticastChannel接口,它为DatagramChannel提供了IP多播的支持,以及对应的异步支持。
总结
本系列文章介绍了即将发布的JDK 7包含的一些新特性,新的里程碑版本可能很快就会发布,你现在就可以尝试一下这些新特性,也许Oracle/Sun将会增加更多的新特性,如JWebPane浏览器组件,因为之前Sun就曾用闭包让我们惊讶过一次了。