您的位置:首页 > 编程语言 > Java开发

JAVA中NIO的使用方法及其功能详解

2012-03-12 11:37 876 查看
简介

Java世界中的两类IO:IO(性能瓶颈)和NIO以及jdk1.7中要加入的增强版NIO

•IO:面向流的方式处理数据(单个的字节,字符的移动,流的一次操作一次只能产生或者消费一个字节或者字符即使有缓冲,也需要程序员自己填充和提取缓冲区内容)
•NIO:面向块的方式处理数据(数据块的移动,一次操作产生或者消费一个数据块,将最耗时的 I/O 操作--填充和提取缓冲区内容操作转移回操作系统)
NIO的特点:
NIO包引入了四个关键的抽象数据类型
1)Buffer:它是包含数据且用于读写的线形表结构(字节数组)。其中还提供了一个特殊类用于内存映射文件的I/O操作。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
2)Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作(编码及解码器)。
3)Channels:通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。包含socket,file和pipe三种管道,它实际上是双向交流的通道(与流的不同之处在)。



4)Selector:它将多元异步I/O操作集中到一个或多个线程中
[align=center]Buffer类类图[/align]



[align=left]NIO中BUFFER的属性[/align]

•所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。
•容量(Capacity) 缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
• 上界(Limit) 缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
•位置(Position) 下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )方法更新。
•标记(Mark) 一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。
•这四个属性之间总是遵循以下关系: 0 <= mark <= position <= limit <= capacity 让我们来看看这些属性在实际应用中的一些例子。
•新创建的容量为 10的 ByteBuffer 逻辑视图



位置被设为 0,而且容量和上界被设为 10,刚好经过缓冲区能够容纳的最后一个字节。标记最初未定义。容量是固定的,但另外的三个属性可以在使用缓冲区时改变

public abstract class Buffer {//没有get(),put()方法,但是子类中有
public final int capacity()
public final int position()
public final Buffer position(int newPositio) //定位位置
public final int limit()
public final Buffer limit (int newLimit)//定位上界
public final Buffer mark() //将标记设为当前位置的值
public final Buffer reset() //将位置设为当前的标记值,如果标记值未定义将抛出异常
public final Buffer clear() //重置方法,将缓冲区置为填充状态即将position设置为 0。将 limit 设置为与 capacity 相同。
public final Buffer flip() //翻转方法,将缓冲区填充状态翻转成释放状态即    将 limit 设置为当前 position。将 position 设置为 0。实现细:buffer.limit(buffer.position()).position(0);
public final Buffer rewind()//不影响上界属性。只是将位置值设回 0
public final int remaining() //从当前位置到上界还剩余的元素数目
public final boolean hasRemaining()//是否已经达到缓冲区的上界
public abstract boolean isReadOnly(); //判断缓冲区是否仅可读,修改只读缓冲区将会抛出异常
}

应用实例——复制文件的操作:



•读取文件操作:(1) 从 FileInputStream 获取Channel, (2) 创建Buffer, (3) 将数据从 Channel 读到 Buffer中.
•写入文件操作:(1)从 FileOutputStream 获取Channel, (2)创建Buffer, (3)将数据从 Channel 写到 Buffer中.
•重设缓冲区操作:在从输入通道读入缓冲区之前,我们调用 clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用 flip() 方法
•检查状态操作: read() 方法返回 -1 是判断文件读取完成的标志
•注意:不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入
 
NIO的三大特性:
1、分散与聚集读取

2、文件锁定功能

3、网络异步IO

 
1.分散与聚集读取
要了解分散与聚集的读取,我们首先应该先了解一下磁盘的I/O,那么磁盘的I/O是以一种什么方式输入输出的呢?

         当进程请求 I/O 操作的时候,它执行一个系统调用,将控制权移交给内核。底层函数 open( )、read( )、write( )和 close( )要做的无非就是建立和执行适当的系统调用。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进 内存。
那么为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢? 首先, 硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。



分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。
一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。  进程只需一个系统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来



 

如何实现:通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。
一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:
long read( ByteBuffer[] dsts );long read( ByteBuffer[] dsts, int offset, int length );
一个 GatheringByteChannel是一个具有两个附加写方法的通道:
long write( ByteBuffer[] srcs );long write( ByteBuffer[] srcs, int offset, int length );
 
2.文件锁定功能

 

3.网络异步IO
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO
•阻塞(线程被挂起)IO:
      socket 的阻塞模式意味着必须要做完IO 操作(包括错误)才会返回。

•非阻塞(线程不会被挂起)IO:
      非阻塞模式下无论操作是否完成都会立刻返回,需要通过其他方式来判断具体操作是否成功。

•同步IO:
     IO操作将导致请求进程阻塞,直到IO操作完成。

•异步IO:
    IO操作不导致请求进程阻塞
•两者的区别就在于synchronous IO做"IO operation”(真实的IO操作)的时候会将process阻塞。
•阻塞和非阻塞就是进程的两种状态。
•同步和异步是只跟IO操作过程中进程的状态变化有关
•同步=/=阻塞,异步= /=非阻塞
•IO模型(5种之多):阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO
•以linux 网络IO的read举例,介绍这些IO模型,其中整个过程涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,经历两个阶段:

   1 等待数据准备 

   2 将数据从内核拷贝到进程中
•同步IO,需要用户进程主动将存放在内核缓冲区中的数据拷贝到用户进程中。
•异步IO,内核会自动将数据从内核缓冲区拷贝到用户缓冲区,然后再通知用户。



 



 



 



 
IO模型比较

•在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用 recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
举例:有A,B,C,D四个人在钓鱼:

A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;

B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;

C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;

D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信
 
Socket通道

•新的socket通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序巨大的可伸缩性和灵活性。再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。
•全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象( java.net的类(Socket、ServerSocket和DatagramSocket) )
• Socket通道将与通信协议相关的操作委托给相应的socket对象,两者对等能够相互获得(Channel对象的socket()方法和socket对象的getChannel ()方法)。但是如果直接从net包的socket对象来获得相应的Channel对象将会返回null,只有从NIO新包入口才能体验新特性。
 
选择器

•选择器,可选择通道和选择键类
•选择器(Selector) :选择器类管理着一个被注册的通道集合的信息和它们的就绪状态
•可选择通道(SelectableChannel) :提供了实现通道的可选择性所需要的公共方法,可以被注册到一个或多个选择器上
•选择键(SelectionKey) :选择键封装了特定的通道与特定的选择器之间的注册关系
三者的关系:



 
public abstract class SelectableChannel extends AbstractChannel implements Channel{
public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException;//将可选择通道注册到选择器上,返回表示二者关系的SelectionKey 对象,如果选择器关闭或者通道为阻塞模式则会抛出异常,第二个参数表示所关心的通道操作,在JDK 1.4中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。
public abstract boolean ( ) isRegistered( );//判断是否注册到选择器上
public abstract SelectionKey key  isRegisteredFor (Selector sel);//判断是否注册到特定的选择器上
public abstract int validOps( );//获取特定的通道所支持的操作集合
public abstract void configureBlocking (boolean block) throws IOException;
public abstract boolean isBlocking( );//来配置并检查通道的阻塞模式
public abstract Object blockingLock( );
}
public abstract class Selector{
public static Selector open() throws IOException;静态工厂方法来实例化Selector 对象
public abstract boolean isOpen( );
public abstract void close( ) throws IOException; 释放资源和设置选择键无效
public abstract SelectionProvider provider( ); SelectionProvider对象用于创建一个Selector对象
public abstract int select( ) throws IOException;
public abstract int select (long timeout) throws IOException;是阻塞的,有三种条件可以停止阻塞:1)至少存在一条通道是ready I/O的;2)等待超时;3)被唤醒,如被调用wakeup。返回值即为ready I/O的通道数量
public abstract int selectNow( ) throws IOException;是非阻塞的,返回值即为ready I/O的通道数量,无ready则返回0
public abstract void wakeup( );
public abstract Set keys( ); //返回已注册的键的集合
public abstract Set selectedKeys( );//返回已选择的键的集合
}




 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息