摘要: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对准备就绪的通道做解决:

  1. 调用select()办法获取已就绪的通道,返回的int值示意有多少通道曾经就绪
  2. 从selector中获取selectedkeys
  3. 遍历selectedkeys
  4. 查看各SelectionKey中 是否有事件就绪了。
  5. 如果有事件就绪,从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。

点击关注,第一工夫理解华为云陈腐技术~