[toc]
Q: NIO和标准IO有什么区别?#
A:
标准IO, 基于字节流和字符流进行操作,阻塞IO。
NIO基于通道channel和缓冲区Buffer进行操作,支持非阻塞IO,提供选择器
JavaNIO核心3组件:#
1. Channels 通道#
Q: 通道Channel对象能同时做读写操作吗?#
还是说需要像标准IO那样,需要同时创建input和output对象才能做读写操作?
A:
通道Channel是双向的, 既可以从channel中读数据,也可以写数据。
可以看到既能调用read也能调用write,且需要依赖缓冲区buffer。
1 | FileChannel fileChannel = FileChannel.open(new File("a.txt").toPath()); |
- 注意上图上,fileChannel.read(buf)是将a.txt里的数据读到buf, 即a.txt->buf
- fileChannel.write(buf)是将buf里的数据写入到a.txt中, 即buf->a.txt,不要搞反啦!
- 通道和缓冲区的关系如下图:
Q: 通道支持异步读写吗#
A: 支持。
Q: 通道的读写是否必须要依赖缓冲区buffer?#
A: 一般都是依赖buffer的。 但也支持2个管道之间的传输,即管道之间直接读写。
1 | String[] arr=new String[]{"a.txt","b.txt"}; |
Q: 有哪几种常用的NIO Channel#
A:
- FileChannel
Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
创建方式
1 | RandomAccessFile file = new RandomAccessFile("D:/aa.txt"); |
- SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
支持非阻塞模式socketChannel.configureBlocking(false)。
可以通过以下2种方式创建SocketChannel:
打开一个SocketChannel并连接到互联网上的某台服务器。
一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
创建方式:
1 | SocketChannel socketChannel = SocketChannel.open(); |
- ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。ServerSocketChannel类在 java.nio.channels包中。
SocketChannel和ServerSocketChannel的区别: 前者用于客户端,后者用于服务端
创建方式:
1 | ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); |
Q: fileChannal为什么不支持非阻塞?#
A:
- UNIX不支持文件的非阻塞I / O,请参见 Non-blocking I/O with regular files.由于Java应该(至少尝试)在所有平台上提供相同的行为,所以FileChannel不会实现SelectableChannel.
- 阻塞与非阻塞发生在进程与进程之间的通信,一般文件的读写操作只会发生在进程内部,不存在外部进程的传输。
即使存在进程间的传输,文件依旧需要通过网络通信,把内容转成数据流传输,这点的传输和Socket通信是相同的,所以没必要再造一个API出来
2. Buffer缓冲区#
buffer特点详解#
- 我们真正要把数据拿到或者要写数据, 实际上都是通过buffer进行操作的。
文件 <-> buffer <-> 数据 - buffer是1个即可读也可写的缓冲区,拥有读写2种模式。
- buffer的capacity属性限定了每个buffer的最大容量,下面的1024就是capacity。
ByteBuffer buf = ByteBuffer.allocate(1024); - buffer拥有1个position属性,表示当前的读写位置。
- 往buffer中写数据时,position就会增加。
- position最大值为capacity-1
- 把fileChannel对应文件里的数据 写入到buffer,叫做写模式
- 写之后,调用flip,让buffer的postion置0,此时相当于准备读取buffer里的数据(即调用buffer.get()拿数据)
(这个模式的叫法个人也觉得不太好,很容易绕,你可以就记忆成: flip就是从写模式转成读模式!)
Q: buffer调用flip()方法从写模式切换到读模式时,position会变成多少?#
A: 变为0。
1 | ByteBuffer buf = ByteBuffer.allocate(1024); |
Q: buffer拥有1个limit属性。知道是做什么的吗?#
A:
写模式下,buffer的limit就是buffer的capacity,即最大容量
Q: 当buffer从写模式切换到读模式时,limit为多少?#
A: 每次切换前都要调用flip(),切换后,limit为写模式中的position。
1 | int byteRead = fileChannel.read(buf); |
结果如下
Q: 向buf缓冲区写数据的方式有哪些?#
A:
- int byteRead = fileChannel.read(buf);
从通道中读数据到buf中, 即相当于向buf缓冲区中写数据。 - buf.putChar(‘a’);
手动向buf中写入字符a, postion加1。
Q: 手动修改当前缓冲区的postion的方法有哪些?#
A:
rewind() 将postion设置为0
mark() 可以标记1个特定的位置, 相当于打标记, 在一顿操作后,可通过reset()回到之前mark()的位置(就像你需要mark我的这几篇博文一样!)
Q:1个channel管道支持多个buffer吗?#
A: 支持。 通道的write和read方法都支持传入1个buffer数组,会按照顺序做读写操作。
Q: 讲一下buffer的种类
A:
Buffer的种类:
大部分都是java基础类型产生的buffer, 除了mappedByteBuffer比较特别。
Q: Buffer的Warp、slice、duplicate分别是做什么的?#
A:
-
warp:
根据一个byte[]来生成一个固定的ByteBuffer时,使用ByteBuffer.wrap()非法的合适。他会直接基于byte[]数组生成一个新的buffer,值也保持一致。 -
slice:
得到切片后的数组。 -
duplicate:
调用duplicate方法返回的Buffer对象就是复制了一份原始缓冲区,复制了position、limit、capacity这些属性
注意!!!!!!
以上warp\slice\duplicte生成的缓冲区get和put所操作的数组还是与原始缓冲区一样的
所以对复制后的缓冲区进行修改也会修改原始的缓冲区,反之亦然
因此duplicte、slice一般是用于操作一下poistion\limit等处理,但是原内容不会去变他,否则就会引起原缓冲器的修改。
Selector#
selector可用来在线程中关联多个通道,并进行事件监听。
Q: 在NIO中Selector的好处是什么?#
A:
可以用更少的线程来管理各个通道。
减少线程上下文切换的资源开销。
Q: Selector支持注册哪种类型的通道?#
A:
支持非阻塞的通道。
通道要在注册前调用 channel.configureBlocking(false) 设置为非阻塞。
例如FileChannel就没办法注册,他注定是阻塞的。
而socketChannel就可以支持非阻塞。
Q: Selector注册时,支持监听哪几种事件,对应的常量是什么?(啊最不喜欢记忆这种东西了…)#
A:
共有4种可监听事件
- Connect 成功连接到1个服务器,对应常量SelectionKey.OP_CONNECT
- Accept 准备好接收新进入的连接, 对应常量SelectionKey.OP_ACCEPT
- Read, 有数据可读,对应常量SelectionKey.OP_READ
- Write 接收到往里写的数据, 对应常量SelectionKey.OP_WRITE
如果希望对该通道监听多种事件,可以用"|"位或操作符把常量连接起来。
1 | int interestingSet = Selectionkey.OP_READ | Selectionkey.OP_WRITE; |
SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系
Q: Selector维护的SelectionKey集合共有哪几种?#
A:
共有三种。
-
已注册的所有键的集合(Registered key set)
所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。 -
已选择的键的集合(Selected key set)
已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。
不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。 -
已取消的键的集合(Cancelled key set)
已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。
Q:注册之后, 如何使用selector对准备就绪的通道做处理:#
A:
- 调用select()方法获取已就绪的通道,返回的int值表示有多少通道已经就绪
- 从selector中获取selectedkeys
- 遍历selectedkeys
- 查看各SelectionKey中 是否有事件就绪了。
- 如果有事件就绪,从key中获取对应对应管道。做对应处理
- 类似如下,一般都会启1个线程来run这个selector监听的处理
Q:select()方法其实是阻塞方法,即调用时会进入等待,直到把所有通道都轮询完毕。如果希望提前结束select(),有哪些方法?#
A:
有2个办法:
wakeup(), 调用后,select()方法立刻返回。
close(), 直接关闭selector。
Q:之前说NIO是非阻塞IO,但为什么上面却说select()方法是阻塞的?#
A:
其实NIO的非阻塞,指的是IO不阻塞,即我们不会卡在read()处,我们会用selector去查询就绪状态,如果状态ok就。
而查询操作是需要时间,因此select()必须要把所有通道都检查一遍才能告诉结果,因此select这个查询操作是阻塞的。
NIO进阶#
Q: 多线程读写同一文件时,如何加锁保证线程安全?#
A:
使用FileChannel的加锁功能。
1 | RandomAccessFile randFile = new RandomAccessFile(target, "rw"); |
Q: 如果需要读1个特大文件,可以使用什么缓冲区?#
A:
使用MappedByteBuffer。
这个缓冲区可以把大文件理解成1个byte数组来访问(但实际上并没有加载这么大的byte数组,实际内容放在内存+虚存中)。
主要通过FileChannel.map(模式,起始位置,区域)来生成1个MappedByteBuffer。
然后可以用put和get去处理对应位置的byte。
1 | int length = 0x8FFFFFF;//一个byte占1B,所以共向文件中存128M的数据 |
MapMode的三种模式:
- MapMode.READ_ONLY(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException。
- MapMode.READ_WRITE(读/写): 对得到的缓冲区的更改会写入文件,需要调用fore()方法
- MapMode.PRIVATE(专用): 可读可写,但是修改的内容不会写入文件,只是buffer自身的改变。
Q:NIO中ByteBuffer, 该如何根据正确的编码,转为对应的CharBuffer#
A:
利用Charset的decode功能。
1 | ByteBuffer byteBuffer = ...; |
Q: NIO背后的实现原理是什么?#
A: 使用了linux的epoll机制
NIO和epoll的对应关系见浅谈NIO和Epoll的实现原理
epoll原理见epoll原理