Java NIO总结

一、NIO和I/O区别

I/O和NIO的区别在于数据的打包和传输方式。html

  • I/O流的方式处理数据java

  • NIO块的方式处理数据数组

面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据建立过滤器很是容易。连接几个过滤器,以便每一个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。面向流的 I/O 一般至关慢。服务器

一个 面向块 的 I/O 系统以块的形式处理数据。每个操做都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。可是面向块的I/O比较复杂。NIO的主要应用在高性能、高容量服务端应用程序。网络

IO NIO
面向流 面向块,缓冲
阻塞IO 非阻塞IO
Selector

NIO的核心梳理

一、Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操做的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操做,数据老是从通道读取到缓冲区中,或者从缓冲区写入到通道中。dom

二、Non-blocking IO(非阻塞IO)
Java NIO能够非阻塞的方式使用IO,例如:当线程从通道读取数据到缓冲区时,线程仍是能够进行其余事情。当数据被写入到缓冲区时,线程能够继续处理它。从缓冲区写入通道也相似。异步

三、Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(好比:链接打开,数据到达)。所以,单个的线程能够监听多个数据通道。socket

二、Buffers(缓冲区)

通道和缓冲区是 NIO 中的核心对象,几乎在每个 I/O 操做中都要使用它们。性能

一个 Buffer 实质上是一个容器对象,它包含一些要写入或者刚读出的数据。spa

在 NIO 库中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任什么时候候访问 NIO 中的数据,您都是将它放到缓冲区中。

缓冲区实质上就是一个数组,一般它是一个字节数组,可是也可使用其余种类的数组。但它不只仅是一个数组,缓冲区还提供了对数据的结构化访问,并且还能够跟踪系统的读/写进程。

Buffer 类型

最经常使用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 能够在其底层字节数组上进行 get/set 操做(即字节的获取和设置)。

ByteBuffer 不是 NIO 中惟一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:
image_1bc2m32k0rp7147du6n95f1fjg9.png-25.5kB

Buffer抽象类中提供了须要处理的方法类型。

Buffer 用法

使用Buffer读写数据通常遵循如下四个步骤:

  1. 写入数据到Buffer

  2. 调用flip()方法

  3. 从Buffer中读取数据

  4. 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,须要经过flip()方法将Buffer从写模式切换到读模式。在读模式下,能够读取以前写入到buffer的全部数据。一旦读完了全部的数据,就须要清空缓冲区,让它能够再次被写入。

有两种方式能清空缓冲区:调用clear()或compact()方法。

clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

try (RandomAccessFile aFile = new RandomAccessFile("F:\\1.txt", "rw")) {
            //从RandomAccesFile获取通道
            FileChannel inChannel = aFile.getChannel();
            
            //建立一个缓冲区并在其中放入一些数据
            ByteBuffer buf = ByteBuffer.allocate(48);
            int bytesRead = inChannel.read(buf); //read into buffer.
            
            //检查状态
            while (bytesRead != -1) {
                //flip() 方法让缓冲区能够将新读入的数据写入另外一个通道。
                buf.flip();  //make buffer ready for read
                
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get()); // read 1 byte at a time
                }
                
                buf.clear(); //make buffer ready for writing
                bytesRead = inChannel.read(buf);
            }
        }

capacity & position & limit

缓冲区本质上是一块能够写入数据,而后能够从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

缓冲区对象有三个基本属性:

  • 容量Capacity:缓冲区能容纳的数据元素的最大数量,在缓冲区建立时设定,没法更改

  • 上界Limit:代表还有多少数据须要取出(在从缓冲区写入通道时),或者还有多少空间能够放入数据(在从通道读入缓冲区时),limit不能大于capacity

  • 位置Position:下一个要被读或写的元素的索引

这三个变量一块儿能够跟踪缓冲区的状态和它所包含的数据。

四个属性老是遵循这样的关系:0<=mark<=position<=limit<=capacity。下图是新建立的容量为10的缓冲区逻辑视图:
15184237_ffbM

写入模式
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
五次调用put后的缓冲区:
15184241_NUYi

此时,limit表示还有多少空间能够放入数据。position最大可为capacity-1。

读取模式
如今缓冲区满了,咱们必须将其清空。咱们想把这个缓冲区传递给一个通道,以使内容能被所有写出,但如今执行get()无疑会取出未定义的数据。咱们必须将posistion设为0,而后通道就会从正确的位置开始读了,但读到哪算读完了呢?这正是limit引入的缘由,它指明缓冲区有效内容的未端。这个操做 在缓冲区中叫作翻转:buffer.flip()。

flip这个方法作两件很是重要的事:

  1. 将 limit 设置为当前 position。

  2. 将 position 设置为 0。

15184249_V47C

rewind操做与flip类似,但不影响limit。

clear
最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。
Clear 作两种很是重要的事情:

1.将 limit 设置为与 capacity 相同。
2.设置 position 为 0。
15184237_ffbM

clear()与compact()区别
一、clear()方法,position将被设回0,limit被设置成 capacity的值。Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”
二、compact()方法将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素正后面。limit属性依然像clear()方法同样,设置成capacity。

Buffer的分配

在可以读和写以前,必须有一个缓冲区。要建立缓冲区,必需要进行分配,。咱们使用静态方法 allocate() 来分配缓冲区,每个Buffer类都有一个allocate方法。

//分配48字节capacity的ByteBuffer的例子。
ByteBuffer buf = ByteBuffer.allocate(48);

//分配一个可存储1024个字符的CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);

Buffer写入

写数据到Buffer有两种方式:

  1. 从Channel写到Buffer。

  2. 经过Buffer的put()方法写到Buffer里。

//从Channel写到Buffer
int bytesRead = inChannel.read(buf); //read into buffer.

//经过put方法写Buffer的例子:
buf.put(127);

Buffer读取

从Buffer中读取数据有两种方式:

  1. 从Buffer读取数据到Channel。

  2. 使用get()方法从Buffer中读取数据。

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

//使用get()方法从Buffer中读取数据
byte aByte = buf.get();

get方法有不少版本,容许你以不一样的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。

rewind()方法

Buffer.rewind()将position设回0,因此你能够重读Buffer中的全部数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。

mark()与reset()

经过调用Buffer.mark()方法,能够标记Buffer中的一个特定position。以后能够经过调用Buffer.reset()方法恢复到这个position。

Buffer比较

equals()与compareTo()方法

可使用equals()和compareTo()方法两个Buffer。

equals()

当知足下列条件时,表示两个Buffer相等:

  • 有相同的类型(byte、char、int等)。

  • Buffer中剩余的byte、char等的个数相等。

  • Buffer中全部剩余的byte、char等都相同。

equals只是比较Buffer的一部分,不是每个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

compareTo()

compareTo()方法比较两个Buffer的剩余元素(byte、char等), 若是知足下列条件,则认为一个Buffer“小于”另外一个Buffer:

  • 第一个不相等的元素小于另外一个Buffer中对应的元素

  • 第一个Buffer的元素个数比另外一个少

三、Channel

Java NIO的通道相似流,但又有些不一样:

  1. 既能够从通道中读取数据,又能够写数据到通道。但流的读写一般是单向的。

  2. 通道能够异步地读写。

  3. 通道中的数据老是要先读到一个Buffer,或者老是要从一个Buffer中写入。

Channel类型

image_1bca9kutq1dno13o3oeqk52brg13.png-64.7kB

  • FileChannel 从文件中读写数据。

  • DatagramChannel 能经过UDP读写网络中的数据。

  • SocketChannel 能经过TCP读写网络中的数据。

  • ServerSocketChannel能够监听新进来的TCP链接,像Web服务器那样。对每个新进来的链接都会建立一个SocketChannel。

close Channel

与缓冲区不一样,通道不能被重复使用;关闭通道后,通道将再也不链接任何东西,任何的读或写操做都会致使ClosedChannelException。

调用通道的close()方法时,可能会致使线程暂时阻塞,就算通道处于非阻塞模式也不例外。若是通道实现了InterruptibleChannel接 口,那么阻塞在该通道上的一个线程被中断时,该通道将被关闭,被阻塞线程也会抛出ClosedByInterruptException异常。

当一个通道 关闭时,休眠在该通道上的全部线程都将被唤醒并收到一个AsynchronousCloseException异常。

Scatter/Gather

scatter/gather用于描述从Channel中读取或者写入到Channel的操做。

  • 分散(scatter)从Channel中读取是指在读操做时将读取的数据写入多个buffer中。所以,Channel将从Channel中读取的数据"分散(scatter)"到多个Buffer中。

  • 汇集(gather)写入Channel是指在写操做时将多个buffer的数据写入同一个Channel,所以,Channel 将多个Buffer中的数据"汇集(gather)"后发送到Channel。

scatter / gather常常用于须要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不一样的buffer中,这样你能够方便的处理消息头和消息体。

Scatter Reader

image_1bca92dvmnck1g971sn61vkon0f9.png-10.4kB

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

buffer首先被插入到数组,而后再将数组做为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另外一个buffer中写。

Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,若是存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工做。

Gathering Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。以下图描述:
image_1bca94vq9kmk1n11tfmug61dmjm.png-9.9kB

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。所以,若是一个buffer的容量为128byte,可是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。所以与Scattering Reads相反,Gathering Writes能较好的处理动态消息。

FileChannel

Java NIO中的FileChannel是一个链接到文件的通道。能够经过文件通道读写文件。FileChannel没法设置为非阻塞模式,它老是运行在阻塞模式下。

FileChannel读数据

由于没法保证write()方法一次能向FileChannel写入多少字节,所以须要重复调用write()方法,直到Buffer中已经没有还没有写入通道的字节。

//经过inputstream或者RandomAccessFile,打开FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//往buffer里面读取数据
ByteBuffer buf = ByteBuffer.allocate(48);

//int值表示了有多少字节被读到了Buffer中。若是返回-1,表示到了文件末尾。
int bytesRead = inChannel.read(buf);

FileChannel写数据

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

//用完FileChannel后必须将其关闭
channel.close();

四、Selector

Selector(选择器)是Java NIO中可以检测一到多个NIO通道,并可以知晓通道是否为诸如读写事件作好准备的组件。这样,一个单独的线程能够管理多个channel,从而管理多个网络链接。

对于操做系统来讲,线程之间上下文切换的开销很大,并且每一个线程都要占用系统的一些资源(如内存)。所以,使用的线程越少越好。

Selector的建立与注册

使用Selector.open()方法建立Selector,为了将Channel和Selector配合使用,必须将channel注册到selector上。经过SelectableChannel.register()方法来实现。以下所示

//建立Selector
Selector selector = Selector.open();

//向Selector注册通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

与Selector一块儿使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一块儿使用,由于FileChannel不能切换到非阻塞模式。而套接字通道均可以。

register()方法的第二个参数。这是一个“interest集合”,意思是在经过Selector监听Channel时对什么事件感兴趣。能够监听四种不一样类型的事件:

事件类型 常量
Connect SelectionKey.OP_CONNECT
Accept SelectionKey.OP_ACCEPT
Read SelectionKey.OP_READ
Write SelectionKey.OP_WRITE

通道触发了一个事件意思是该事件已经就绪。因此,某个channel成功链接到另外一个服务器称为“链接就绪”。一个server socket channel准备好接收新进入的链接称为“接收就绪”。一个有数据可读的通道能够说是“读就绪”。等待写数据的通道能够说是“写就绪”。

SelectionKey

当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些属性:

  1. interest集合

  2. ready集合

  3. Channel

  4. Selector

  5. 附加的对象

interest集合

interest集合是你所选择的感兴趣的事件集合。能够经过SelectionKey读写interest集合,像这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

|位与操做interest 集合和给定的SelectionKey常量,能够肯定某个肯定的事件是否在interest 集合中。

ready集合

ready 集合是通道已经准备就绪的操做的集合。在一次选择(Selection)以后,会首先访问这个ready set。能够这样访问ready集合:

int readySet = selectionKey.readyOps();

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

能够用像检测interest集合那样的方法,来检测channel中什么事件或操做已经就绪。可是,也可使用以上四个方法,它们都会返回一个布尔类型。

Channel + Selector

从SelectionKey访问Channel和Selector很简单。以下:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

附加的对象
能够将一个对象或者更多信息附着到SelectionKey上,能够方便识别某个给定的通道。例如,能够附加 与通道一块儿使用的Buffer,或是包含汇集数据的某个对象。使用方法以下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

//用register()方法向Selector注册Channel的时候附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selector选择通道

一旦向Selector注册了一或多个通道,就能够调用select()方法,选择已经准备就绪的事件的通道类型。
select方法

int select() //阻塞到至少有一个通道在你注册的事件上就绪了
int select(long timeout)//和select()同样,除了最长会阻塞timeout毫秒(参数)。
int selectNow()//不会阻塞,无论什么通道就绪都马上返回

select()方法返回的int值表示有多少通道已经就绪,而后能够经过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。以下所示:

//访问“已选择键集(selected key set)”中的就绪通道
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

这个循环遍历已选择键集中的每一个键,并检测各个键所对应的通道的就绪事件。

注意每次迭代末尾的keyIterator.remove()调用。Selector不会本身从已选择键集中移除SelectionKey实例。必须在处理完通道时本身移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

wakeUp()

某个线程调用select()方法后阻塞了,即便没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法便可。阻塞在select()方法上的线程会立马返回。

若是有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会当即“醒来(wake up)”。

close()

用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的全部SelectionKey实例无效。通道自己并不会关闭。

五、 管道(pip)

管道是2个线程之间的单向数据链接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

原理图以下:
image_1bd1glsp81i341t187to84o7j39.png-10.9kB

管道写入数据

//Pipe.open()方法打开管道
Pipe pipe = Pipe.open();

//访问sink通道,向管道写数据
Pipe.SinkChannel sinkChannel = pipe.sink();

//调用SinkChannel的write()方法,将数据写入SinkChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}

管道读取数据

//访问source通道
Pipe.SourceChannel sourceChannel = pipe.source();

//调用source通道的read()方法来读取数据
ByteBuffer buf = ByteBuffer.allocate(48);

//read()方法返回的int值表示多少字节被读进了缓冲区
int bytesRead = sourceChannel.read(buf);

参考资料引用

一、NIO 入门
二、Java NIO教程
三、理解Java NIO

相关文章
相关标签/搜索