一、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)。
使用处理流进行输入输出操作更简单,执行效率更高。
一般来说,字节流的功能比字符流的功能强大,因为计算机是里的数据都是二进制的,而字节流可以处理所有的二进制文件。
如果输入输出的内容是文本内容,就使用字符流;如果输入输出的内容是二进制内容,就使用字节流。
二、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()源码:
Buffer的读/写模式共用一个position和limit变量,当从写模式变为读模式时,原先的position就变成了读模式的limit。
rewind()源码:
这个方法仅仅是将position置为0。
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:
每次迭代时,都调用了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。
完整的示例
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,使用一个单独的线程来选择通道;这种选择机制,使得一个单独的线程很容易来管理多个通道,但是付出的解析数据可能会 比一个阻塞流中读取数据更复杂。