摘要:NIO 即 New IO,这个库是在 JDK1.4 中才引入的。NIO 和 IO 有雷同的作用和目标,但实现形式不同,NIO 次要用到的是块,所以 NIO 的效率要比 IO 高很多。
本文分享自华为云社区《java 中的 NIO 和 IO 到底是什么区别?20 个问题通知你答案【奔跑吧!JAVA】》,原文作者:breakDraw。
NIO 即 New IO,这个库是在 JDK1.4 中才引入的。NIO 和 IO 有雷同的作用和目标,但实现形式不同,NIO 次要用到的是块,所以 NIO 的效率要比 IO 高很多。
Q:NIO 和规范 IO 有什么区别?
A:
- 规范 IO,基于字节流和字符流进行操作,阻塞 IO。
- NIO 基于通道 channel 和缓冲区 Buffer 进行操作,反对非阻塞 IO,提供选择器
§ JavaNIO 外围 3 组件:
§ Channels 通道
Q:通道 Channel 对象能同时做读写操作吗?
还是说须要像规范 IO 那样,须要同时创立 input 和 output 对象能力做读写操作?
A:通道 Channel 是双向的,既能够从 channel 中读数据,也能够写数据。
能够看到既能调用 read 也能调用 write,且须要依赖缓冲区 buffer。
FileChannel fileChannel = FileChannel.open(new File("a.txt").toPath());
ByteBuffer buf = ByteBuffer.allocate(1024);
fileChannel.read(buf);
fileChannel.write(buf);
- 留神上图上,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 个管道之间的传输,即管道之间间接读写。
String[] arr=new String[]{"a.txt","b.txt"};
FileChannel in=new FileInputStream(arr[0]).getChannel();
FileChannel out =new FileOutputStream(arr[1]).getChannel();
// 将 a.txt 中的数据间接写进 b.txt 中,相当于文件拷贝
in.transferTo(0, in.size(), out);
罕用的几种 Channel
- FileChannel
Java NIO 中的 FileChannel 是一个连贯到文件的通道。能够通过文件通道读写文件。FileChannel 无奈设置为非阻塞模式,它总是运行在阻塞模式下
创立形式
RandomAccessFile file = new RandomAccessFile("D:/aa.txt");
FileChannel fileChannel = file.getChannel();
- SocketChannel
Java NIO 中的 SocketChannel 是一个连贯到 TCP 网络套接字的通道。反对非阻塞模式 socketChannel.configureBlocking(false)。能够通过以下 2 种形式创立 SocketChannel:
关上一个 SocketChannel 并连贯到互联网上的某台服务器。一个新连贯达到 ServerSocketChannel 时,会创立一个 SocketChannel
创立形式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("192.168.1.100",80));
- ServerSocketChannel
Java NIO 中的 ServerSocketChannel 是一个能够监听新进来的 TCP 连贯的通道, 就像规范 IO 中的 ServerSocket 一样。ServerSocketChannel 类在 java.nio.channels 包中。SocketChannel 和 ServerSocketChannel 的区别:前者用于客户端,后者用于服务端
创立形式:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket.bind(new InetSocketAddress(80));
serverSocketChannel.configureBlocking(false);
while(true){SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannle != null)
doSomething...
}
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。
ByteBuffer buf = ByteBuffer.allocate(1024);
// 数据读到 buf 中, 并返回数量,每次最多读 1024 个
int byteRead = fileChannel.read(buf);
// 输入 byteRead 的数量,最多为 1024
System.out.println("position=" + buf.position()+", byteRead=" + byteRead);
buf.flip();
// 切换到读模式了,输入 0
System.out.println("position=" + buf.position());
- buffer 领有 1 个 limit 属性。
-
写模式下,buffer 的 limit 就是 buffer 的 capacity。
Q:当 buffer 从写模式切换到读模式时,limit 为多少?
A:每次切换前都要调用 flip(),切换后,limit 为写模式中的 position。int byteRead = fileChannel.read(buf); // 输入 1024
System.out.println("limit=" + buf.limit() + ",postion=" + buf.position());
System.out.println("切换到读模式");
buf.flip();
// 输入 byteRead 数量
System.out.println("limit=" + buf.limit());
后果如下
Q:向 buf 缓冲区写数据的形式有哪些?
A:
int byteRead = fileChannel.read(buf);
从通道中读数据到 buf 中,即相当于向 buf 缓冲区中写数据。
buf.putChar(‘a’);
手动向 buf 中写入字符 a,postion 加 1。
Q:从 buf 缓冲区读数据的形式有哪些?
- int bytesWrite = fileChannel.write(buf)
buf 中的数据写入到管道,即相当于 fileChannel 读取 buf 中的数据。 - byte getByte = buf.get()
手动读取 1 个 buf 中的字符,postion 加 1.
Q: 手动批改以后缓冲区的 postion 的办法有哪些?
A:
- rewind() 将 postion 设置为 0
- mark() 能够标记 1 个特定的地位,相当于打标记,在一顿操作后,可通过 reset()回到之前 mark()的地位(就像你须要 mark 我的这几篇博文一样!)
Q:1 个 channel 管道反对多个 buffer 吗?
A:反对。通道的 write 和 read 办法都反对传入 1 个 buffer 数组,会依照程序做读写操作。
Buffer 的品种:
Buffer 的另外 3 个办法:
- 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
如果心愿对该通道监听多种事件,能够用 ”|” 位或操作符把常量连接起来。
int interestingSet = Selectionkey.OP_READ | Selectionkey.OP_WRITE;
Selectionkey key = channel.register(selector,interestingSet)
- SelectionKey 键示意了一个特定的通道对象和一个特定的选择器对象之间的注册关系
Q:Selector 保护的 SelectionKey 汇合共有哪几种?
A:共有三种。
(1)已注册的所有键的汇合(Registered key set)
所有与选择器关联的通道所生成的键的汇合称为曾经注册的键的汇合。并不是所有注册过的键都依然无效。这个汇合通过 keys()办法返回,并且可能是空的。这个已注册的键的汇合不是能够间接批改的;试图这么做的话将引发 java.lang.UnsupportedOperationException。
(2)已抉择的键的汇合(Selected key set)
已注册的键的汇合的子集。这个汇合的每个成员都是相干的通道被选择器 (在前一个抉择操作中) 判断为曾经筹备好的,并且蕴含于键的 interest 汇合中的操作。这个汇合通过 selectedKeys()办法返回 (并有可能是空的)。
不要将已抉择的键的汇合与 ready 汇合弄混了。这是一个键的汇合,每个键都关联一个曾经筹备好至多一种操作的通道。每个键都有一个内嵌的 ready 汇合,批示了所关联的通道曾经筹备好的操作。键能够间接从这个汇合中移除,但不能增加。试图向已抉择的键的汇合中增加元素将抛出 java.lang.UnsupportedOperationException。
(3)已勾销的键的汇合(Cancelled key set)
已注册的键的汇合的子集,这个汇合蕴含了 cancel()办法被调用过的键(这个键曾经被有效化),但它们还没有被登记。这个汇合是选择器对象的公有成员,因此无奈间接拜访。
注册之后,如何应用 selector 对准备就绪的通道做解决:
- 调用 select()办法获取已就绪的通道,返回的 int 值示意有多少通道曾经就绪
- 从 selector 中获取 selectedkeys
- 遍历 selectedkeys
- 查看各 SelectionKey 中 是否有事件就绪了。
- 如果有事件就绪,从 key 中获取对应对应管道。做对应解决
相似如下,个别都会启 1 个线程来 run 这个 selector 监听的解决:
while(true) {int readyNum = selector.select();
if (readyNum == 0) {continue;}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()) {SelectionKey key = it.next();
if(key.isAcceptable()) {// 承受连贯} else if (key.isReadable()) {// 通道可读} else if (key.isWritable()) {// 通道可写}
it.remove();}
}
Q:select()办法其实是阻塞办法,即调用时会进入期待,直到把所有通道都轮询结束。如果心愿提前结束 select(),有哪些办法?
A:有 2 个方法:
wakeup(),调用后,select()办法立即返回。
close(),间接敞开 selector。
PS: 之前说 NIO 是非阻塞 IO,但为什么下面却说 select()办法是阻塞的?
- 其实 NIO 的非阻塞,指的是 IO 不阻塞,即咱们不会卡在 read()处,咱们会用 selector 去查问就绪状态,如果状态 ok 就。
- 而查问操作是须要工夫,因而 select()必须要把所有通道都查看一遍能力通知后果,因而 select 这个查问操作是阻塞的。
§ 其余
Q:多线程读写同一文件时,如何加锁保障线程平安?
A:应用 FileChannel 的加锁性能。
RandomAccessFile randFile = new RandomAccessFile(target, "rw");
FileChannel channel = randFile.getChannel();
// pos 和 siz 决定加锁区域,shared 指定是否是共享锁
FileLock fileLock = channel.lock(pos , size , shared);
if (fileLock!=null) {do();
// 这里简化了,实际上应该用 try-catch
fileLock.release();}
Q:如果须要读 1 个特大文件,能够应用什么缓冲区?
A:应用 MappedByteBuffer。
这个缓冲区能够把大文件了解成 1 个 byte 数组来拜访(但实际上并没有加载这么大的 byte 数组,理论内容放在内存 + 虚存中)。
次要通过 FileChannel.map(模式,起始地位,区域)来生成 1 个 MappedByteBuffer。而后能够用 put 和 get 去解决对应地位的 byte。
int length = 0x8FFFFFF;// 一个 byte 占 1B,所以共向文件中存 128M 的数据
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE);) {MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
for(int i=0;i<length;i++) {mapBuffer.put((byte)0);
}
for(int i = length/2;i<length/2+4;i++) {
// 像数组一样拜访
System.out.println(mapBuffer.get(i));
}
}
三种模式:
- MapMode.READ_ONLY(只读):试图批改失去的缓冲区将导致抛出 ReadOnlyBufferException。
- MapMode.READ_WRITE(读 / 写):对失去的缓冲区的更改会写入文件,须要调用 fore()办法
- MapMode.PRIVATE(专用):可读可写, 然而批改的内容不会写入文件, 只是 buffer 本身的扭转。
Q:NIO 中 ByteBuffer,该如何依据正确的编码,转为对应的 CharBuffer
A:利用 Charset 的 decode 性能。
ByteBuffer byteBuffer = ...;
Charset charset = Charset.forName("UTF-8");
CharBuffer charBuffer = charset.decode(byteBuffer);
如果是 CharBuffer 转 ByteBuffer,就用 charset.encode。
点击关注,第一工夫理解华为云陈腐技术~