资讯专栏INFORMATION COLUMN

Java NIO总结

Anshiii / 1954人阅读

摘要:当数据被写入到缓冲区时,线程可以继续处理它。当满足下列条件时,表示两个相等有相同的类型等。调用通道的方法时,可能会导致线程暂时阻塞,就算通道处于非阻塞模式也不例外。当一个通道关闭时,休眠在该通道上的所有线程都将被唤醒并收到一个异常。

1、NIO和I/O区别

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

I/O流的方式处理数据

NIO块的方式处理数据

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

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

IO NIO
面向流 面向块,缓冲
阻塞IO 非阻塞IO
Selector
NIO的核心梳理

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

2、Non-blocking IO(非阻塞IO)
Java NIO可以非阻塞的方式使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

3、Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

2、Buffers(缓冲区)

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

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

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

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

Buffer 类型

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

ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:

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

Buffer 用法

使用Buffer读写数据一般遵循以下四个步骤:

写入数据到Buffer

调用flip()方法

从Buffer中读取数据

调用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的缓冲区逻辑视图:

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

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

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

flip这个方法做两件非常重要的事:

将 limit 设置为当前 position。

将 position 设置为 0。

rewind操作与flip相似,但不影响limit。

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

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

clear()与compact()区别
1、clear()方法,position将被设回0,limit被设置成 capacity的值。Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”
2、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有两种方式:

从Channel写到Buffer。

通过Buffer的put()方法写到Buffer里。

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

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

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

从Buffer读取数据到Channel。

使用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的元素个数比另一个少

3、Channel

Java NIO的通道类似流,但又有些不同:

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。

通道可以异步地读写。

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

Channel类型

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

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。如下图描述:

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();
4、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对象。这个对象包含了一些属性:

interest集合

ready集合

Channel

Selector

附加的对象

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实例无效。通道本身并不会关闭。

5、 管道(pip)

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

原理图如下:

管道写入数据
//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);

参考资料引用

1、NIO 入门
2、Java NIO教程
3、理解Java NIO

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66975.html

相关文章

  • 关于Java IO与NIO知识都在这里

    摘要:从通道进行数据写入创建一个缓冲区,填充数据,并要求通道写入数据。三之通道主要内容通道介绍通常来说中的所有都是从通道开始的。从中选择选择器维护注册过的通道的集合,并且这种注册关系都被封装在当中停止选择的方法方法和方法。 由于内容比较多,我下面放的一部分是我更新在我的微信公众号上的链接,微信排版比较好看,更加利于阅读。每一篇文章下面我都把文章的主要内容给列出来了,便于大家学习与回顾。 Ja...

    Riddler 评论0 收藏0
  • 少啰嗦!一分钟带你读懂JavaNIO和经典IO的区别

    摘要:的选择器允许单个线程监视多个输入通道。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动通常不会。 1、引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本文中,将尝试用简明扼要的文字,阐明Java NIO和经典IO之...

    Meils 评论0 收藏0
  • JDK10都发布了,nio你了解多少?

    摘要:而我们现在都已经发布了,的都不知道,这有点说不过去了。而对一个的读写也会有响应的描述符,称为文件描述符,描述符就是一个数字,指向内核中的一个结构体文件路径,数据区等一些属性。 前言 只有光头才能变强 回顾前面: 给女朋友讲解什么是代理模式 包装模式就是这么简单啦 本来我预想是先来回顾一下传统的IO模式的,将传统的IO模式的相关类理清楚(因为IO的类很多)。 但是,发现在整理的过程已...

    YFan 评论0 收藏0
  • Java IO之NIO

    摘要:上篇说了最基础的五种模型,相信大家对相关的概念应该有了一定的了解,这篇文章主要讲讲基于多路复用的。 上篇说了最基础的五种IO模型,相信大家对IO相关的概念应该有了一定的了解,这篇文章主要讲讲基于多路复用IO的Java NIO。 背景 Java诞生至今,有好多种IO模型,从最早的Java IO到后来的Java NIO以及最新的Java AIO,每种IO模型都有它自己的特点,详情请看我的上...

    pingink 评论0 收藏0
  • 一文理解:Java NIO 核心组件

    摘要:的出现解决了这尴尬的问题,非阻塞模式下,通过,我们的线程只为已就绪的通道工作,不用盲目的重试了。注意要将注册到,首先需要将设置为非阻塞模式,否则会抛异常。 showImg(https://segmentfault.com/img/remote/1460000017053374); 背景知识 同步、异步、阻塞、非阻塞 首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下。 ...

    Coding01 评论0 收藏0

发表评论

0条评论

Anshiii

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<