Java的IO和NIO

2019-05-06  本文已影响0人  落地生涯

一、IO

java的IO功能都在java.io包下,包括输入输出两种IO流,每种输入输出流又可分为字节流和字符流两大类。字节流以字节(8位)为单位处理输入输出,字符流以字符(16位)为单位来处理输入输出。

1、流的分类

(1)输入输出流

    主要有InputStream和Reader作为基类,而输出流主要以OutputStream和Writer作为基类,都是抽象类。

(2)字节流和字符流

    字节流:InputStream,OutputStream

    字符流:Reader,Writer

(3)节点流和处理流

    节点流:可以直接从IO设备读取数据的流称为节点流,也称为低级流,比如FileInputStream和FileReader,程序可以直接连接到实际的数据源,构造方法是以物理IO节点作为构造参数的,如FileInputStream(String name);

    处理流:对一个已存在的流进行连接和封装,通过封装后的流来实现数据读写功能,也称为高级流,比如PrintStream;程序并不会直接连接到实际的数据源。程序可以采用完全相同的输入输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际访问的数据源也会相应的发生改变,这就是装饰者模式。处理流的构造方法的参数不是一个物理节点,而是已经存在的流,如PrintStream(OutputStream out)。

    使用处理流进行输入输出操作更简单,执行效率更高。

IO流的分类

    一般来说,字节流的功能比字符流的功能强大,因为计算机是里的数据都是二进制的,而字节流可以处理所有的二进制文件。

    如果输入输出的内容是文本内容,就使用字符流;如果输入输出的内容是二进制内容,就使用字节流。

二、NIO

NIO采用内存映射文件来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。

Channel和Buffer是NIO的两个核心对象。Channel是对传统的输入输出系统的模拟,在NIO系统中所有的数据都需要通过通道传输。

Buffer可以被理解为一个容器,本质是一个数组,发送到Channel中所有的对象都必须首先放到Buffer中,从Channel中读取的数据也必须先放到Buffer中。

NIO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入输出的Selector类,Selector是非阻塞的核心。

1、Buffer

容量(capacity):表示Buffer的大小,创建后不能改变,

界限(limit):第一个不能被读写的缓冲区的位置,也就是后面的数据不能被读写。

位置(position):用于指明下一个可以被读写的缓冲区位置的索引,当从Channel读取数据的时候,position的值就等于读到了多少数据。

Buffer的类型有:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。

Buffer的使用步骤:

    将数据写入Buffer 中,调用Buffer.flip()方法,将NIO Buffer转换为读模式,从Buffer中读取数据,调用Buffer.clear()或者Buffer.compact()方法,将Buffer转换为写模式。

Buffer的作用主要是装入数据,然后输出数据,当装入数据结束时,调用flip()方法,该方法将limit设为position所在位置,将position设为0;输出数据后,调用clear()方法,将position的值设为0,limit设为capacity。

常用的Buffer是CharBuffer和ByteBuffer。使用get()和put()方法进行数据的放入和读取,分为相对和绝对两种:

    相对:从Buffer当前position位置开始读取或者写入数据,然后将position的值按处理元素的个数增加。

    绝对:直接根据索引向Buffer中读取和写入数据,使用绝对方式访问Buffer里的数据时,不会影响position的值。

通过Buffer.allocate()方法创建普通的Buffer,还可以通过allocateDirect()方法来创建直接Buffer,虽然创建成本高,但是读写快,因此试用于长期生存的Buffer。注意,只有ByteBuffer提供了此方法,其他类型想用,可以将该buffer转成其他类型的buffer。

Direct Buffer:

    所分配的内存不在JVM堆上,不受GC管理,但是Direct Buffer的Java对象是由GC管理的,因此发生GC,对象被回收时,Direct Buffer也会被释放。

    因为Direct Buffer不在JVM堆上分配,因此Direct Buffer对应程序的内存占用的影响就那么明显。

    申请和释放Direct Buffer的开销比较大,试用于长期生存的Buffer。

    使用Direct Buffer时,当进行一些底层的系统的IO操作时,效率会比较高,因为此时JVM不需要拷贝buffer中的内存到中间临时缓冲区。

Non-Direct Buffer

    直接在JVM堆上进行内存分配,本质上是byte[]数组的封装。

    因为Non-Direct Buffer在JVM堆中,因此当进行操作系统底层IO操作中,会将此buffer的内存复制到中间临时的缓存中,效率就比较低。

Buffer.rewind()方法可以重置position的值为0,因此我们可以重新读取/写入Buffer;rewind()主要针对读模式,在读模式时,读取到limit后,可以调用rewind()方法,将position置为0。

可以通过Buffer.mark()将当前的position的值保存起来,随后可以调用Buffer.reset()方法将position的值回复出来。

flip(),rewind(),clear()的区别

flip()源码:

flip()源码

       Buffer的读/写模式共用一个position和limit变量,当从写模式变为读模式时,原先的position就变成了读模式的limit。

rewind()源码:

rewind()源码

    这个方法仅仅是将position置为0。

clear()源码:

clear()源码

    将position设为0,将limit设为capacity;

    使用场景:在一个已经写满数据的buffer中,调用clear,可以从头读取buffer的数据;为了将一个buffer填充满数据,可以调用clear,然后一直写入,知道达到limit。

当两个Buffer是相同类型,两个Buffer的剩余的数据个数是相同的,两个Buffer的剩余数据都是相同的,那通过equals()或者compareTo()方法比较两个Buffer时,两个Buffer是相等的。

2、Channel

Channel类似传统的流对象,主要区别是:

    Channel可以直接将指定文件的部分或者全部直接映射成Buffer。

    程序不能直接访问Channel中的数据,只能通过Buffer交互。

    同一个Channel中可以执行读和写,但是一个流对象只支持读或写。

    Channel可以异步的读写,而流对象是阻塞的同步读写。

所有的Channel不应该通过构造器来创建,而是通过传统的InputStream,OutputStream的getChannel()方法来返回对应的Channel,不同节点流获取的Channel不一样。

Channle常用的方法有三类:map(),read(),write()。map()方法将Channel对应的部分或全部数据映射成ByteBuffer;read和write方法都有一系列的重载形势,这些方法用于Buffer中读取/写入数据。

3、Selector

Selector选择器类管理着一个被注册的Channel集合的信息和他们的就绪状态。Channel是和Selector一起被注册的,并且使用选择器来更新通道的就绪状态。利用Selector可使用一个单独的线程管理多个Channel。

当Channel使用register(Selector sel,int ops)方法将通道注册Selector时,Selector对Channel事件进行监听,通过第二个参数指定监听类型。可监听的事件类型包括:

    读:SelectionKey.OP_READ

    写:SelectionKey.OP_WRITE

    连接:SelectionKey.OP_CONNECT

    接收:SelectionKey.OP_ACCEPT

如果需要监听多个事件可以使用 | 操作符,int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE表示同时监听读和写操作。

创建Selector:

    Selector selector = Selector.open();

注册Channel到Selector:(如果一个Channel要注册到Selector中,那么这个Channle必须是非阻塞的,因此FileChannel是不能够使用Selector的,因为FileChannel都是阻塞的。)

    channel.configureBlocking(false);

    SelectionKey key = channel.register(selector,SelectionKet.OP_READ);

一个Channel仅仅可以被注册到一个Selector一次,如果将Channel注册到Selector多次,那么其实就是相当于更新SelectionKey的interest set。

当我们注册一个Channle时,会返回一个SelectionKey对象,这个对象包含:

    interest set:

    ready set:

    channel,selector:

    attrached object(可选的附加对象)

    或者再注册时直接附加:

通过Selector选择Channel:

    可以通过Selector.select()方法获取对某事件准备好了的Channel,select()方法返回的值表示有多少个Channel可操作。

获取可操作性的Channel:

通过selected keySet访问Channle

    每次迭代时,都调用了keyIterator.remove(),将这个key从迭代器中删除,因为select()方法仅仅是简单的将就读的IO操作放到selectedKeys结合中。我们也可以动态更改SelectedKeys中的key的interest set。

Selector的基本流程:

    通过Selector.open()打开一个Selector;

    将Channel注册到Selector中,并设置需要监听的事件;

    不断重复:

        调用select()方法,

        调用selector.selectedKeys()获取selected keys

        迭代每个selected key:

            从key中获取对应的channle和附加信息;

            判断哪些IO事件已经就绪,然后处理;如果是OP_ACCEPT事件,则调用SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(),获取SocketChannle,并将它设置为非阻塞的,然后将这个Channle注册到Selector中;

            根据需要修改selected key的监听事件;

            将已经处理过的key从selected keys中移除。

关闭Selector:当调用Selector.close()方法时,其实是关闭了Selector本身并且将selectionKey失效,但是并不会关闭Channel。

完整的示例

NIO完整示例

4、NIO和IO的区别

NIO是非阻塞的,IO是阻塞的。

NIO提供了Channels 和 Buffers,面向Buffer,通过Channel进行读写,NIO基于Channel和Buffer进行操作,数据总是从Channle读取到Buffer,或者从Buffer到Channle;而标准的IO基于字节流和字符流进行操作,没有缓存的地方。

NIO可以异步使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。NIO将阻塞交给了后台线程执行,NIO的空余时间可以用于其他通道上进行IO,因此一个线程可以管理多个Channel;而IO是阻塞的,阻塞式的IO每个链接必须开一个线程来处理,每个线程都需要CPU资源,等待IO的时候,CPU资源没有被释放就浪费了。

NIO支持Selctor,Selector允许一个单独的线程可以监听多个Channel,可以注册多个Channel到Selector,使用一个单独的线程来选择通道;这种选择机制,使得一个单独的线程很容易来管理多个通道,但是付出的解析数据可能会 比一个阻塞流中读取数据更复杂。

上一篇下一篇

猜你喜欢

热点阅读