Java的NIO

BufferedReader有一个特征,就是读取输入流中的数据时,如果没有读到有效数据,程序将在此处阻塞该线程的执行(使用InputStream的read方法从流中读取数据时,也有这样的特性),java.io下面的输入输出流都是阻塞式的。不仅如此,传统的输入输出流都是通过字节的移动来处理的,及时我们不直接去处理字节流,单底层的实现还是依赖于字节处理,也就是说,面向流的输入输出系统一次只能处理一个字节,因此面向流的输入输出系统通常效率都不高。

为了解决上面的问题,NIO就出现了。NIO采用内存映射文件来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。(这种方式模拟了操作系统上虚拟内存的概念),通过这种方式进行输入输出要快得多。

Channel(通道)和Buffer(缓冲)是NIO中两个核心对象。Channel是对传统的输入输出系统的模拟,在NIO系统中所有的数据都需要通过通道传输,Channel与传统的InputStream、OutputStream最大的区别就是提供了一个map()方法,通过它可以直接将“一块数据”映射到内存中。如果说传统IO是面向流的处理,那么NIO是面向块的处理。

Buffer可以被理解为一个容器,它的本质是一个数组,发送到Channel中所有的对象都必须首先放到Buffer中,从而Channel中读取的数据也必须先放到Buffer中。Buffer既可以一次次从Channel取数据,也可以使用Channel直接将文件的某块数据映射成Buffer。

除了Channel和Buffer之外,NIO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入输出的Selector类

Buffer介绍

在Buffer中有三个重要的概念:

  • 容量(capacity):表示Buffer的大小,创建后不能呢过改变;
  • 界限(limit):第一个不能被读写的缓冲区的位置,也就是后面的数据不能被读写;
  • 位置(position):用于指明下一个可以被读写的缓冲区位置索引。当从Channel读取数据的时候,position的值就等于读到了多少数据。

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

常用的Buffer是CharBuffer和ByteBuffer。

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

  • 相对:从Buffer当前position位置开始读取或者写入数据,然后将position的值按处理元素的个数增加;
  • 绝对:直接根据索引向Buffer中读取和写入数据,使用绝对方式访问Buffer里的数据时,不会影响position的值。

代码示例

package com.wangjun.othersOfJava;

import java.nio.CharBuffer;

public class NIOBufferTest {

    public static void main(String[] args) {
        //创建CharBuffer
        CharBuffer cb = CharBuffer.allocate(8);
        System.out.println("capacity:" + cb.capacity());
        System.out.println("limit:" + cb.limit());
        System.out.println("position:" + cb.position());
        //放入元素
        cb.put('a');
        cb.put('b');
        cb.put('c');
        System.out.println("加入三个元素后,position:" + cb.position());
        cb.flip();
        System.out.println("执行flip()后,limit:" + cb.limit());
        System.out.println("执行flip()后,position:" + cb.position());
        System.out.println("取出第一个元素: " + cb.get());
        System.out.println("取出第一个元素后,position:" + cb.position());
        //调用clear方法
        cb.clear();
        System.out.println("执行clear()后,limit:" + cb.limit());
        System.out.println("执行clear()后,position:" + cb.position());
        System.out.println("执行clear()后,数据没有清空,第三个值是:" + cb.get(2));
        System.out.println("执行绝对读取后,position:" + cb.position());
    }

}

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

Channel(通道)介绍

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

  • Channel可以直接将指定文件的部分或全部直接映射成Buffer。
  • 程序不能直接访问Channel中的数据,只能通过Buffer交互。

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

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

代码示例

package com.wangjun.othersOfJava;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

/*
 * 将FileChannel的全部数据映射成ByteBuffer
 */
public class NIOChannelTest {

    public static void main(String[] args) throws Exception {
        File f = new File("NIOChannelTest.md");
        //java7新特性try括号内的资源会在try语句结束后自动释放,前提是这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。
        try(
            FileChannel inChannel = new FileInputStream(f).getChannel();
            FileChannel outChannel = new FileOutputStream("a.md").getChannel();
                ){
            //将FileChannel的全部数据映射成ByteBuffer
            //map方法的三个参数:1映射模式;2,3控制将哪些数据映射成ByteBuffer
            MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
            //直接将buffer的数据全部输出,完成文件的复制: NIOChannelTest.md -> a.md
            outChannel.write(buffer);
            
            //以下为了输出文件字符串内容
            //使用GBK字符集来创建解码器
            Charset charset = Charset.forName("GBK");
            //复原limit和position的位置
            buffer.clear();
            //创建解码器
            CharsetDecoder decoder = charset.newDecoder();
            //使用解码器将ByteBuffer转成CharBuffer
            CharBuffer charBuffer = decoder.decode(buffer);
            System.out.println(charBuffer);
        }
    }

}

除了上面的一次性取数据,也可以分多次取数据

//如果Channel对应的文件过大,使用map方法一次性将所有文件内容映射到内存中可能会因此性能下降
//这时候我们可以适应Channel和Buffer进行多次重复取值
public static void getDataByArray() throws Exception {
  try(
    //创建文件输入流
    FileInputStream fileInputStream = new FileInputStream("NIOChannelTest.md");
    FileChannel fileChannel = fileInputStream.getChannel();
  ){
    //定义一个ByteBuffer对象,用于重复取水
    ByteBuffer bbuff = ByteBuffer.allocate(64);
    while(fileChannel.read(bbuff) != -1) {
      bbuff.flip();
      Charset charset = Charset.forName("GBK");
      //创建解码器
      CharsetDecoder decoder = charset.newDecoder();
      //使用解码器将ByteBuffer转成CharBuffer
      CharBuffer charBuffer = decoder.decode(bbuff);
      System.out.println(charBuffer);
      //为下一次读取数据做准备
      bbuff.clear();
    }
  }
}

文件锁

在NIO中,Java提供了文件锁的支持,使用FileLock来支持文件锁定功能,在FileChannel中提供lock()/tryLock()方法来获取文件锁FileLock对象,从而锁定文件。lock和tryLock的区别是前者无法得到文件锁的时候会阻塞,后者不会阻塞。也支持锁定部分内容,使用lock(long position, long size, boolean shared)即可,其中shared为true时,表明该锁是一个共享锁,可以允许多个县城读取文件,但阻止其他进程获得该文件的排他锁。当shared为false时,表明是一个排他锁,它将锁住对该文件的读写。

默认获取的是排他锁。

代码示例

package com.wangjun.othersOfJava;

import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockTest {
    public static void main(String[] args) throws Exception {
        try(
            FileOutputStream fos = new FileOutputStream("a.md");
            FileChannel fc = fos.getChannel();
                ){
            //使用非阻塞方式对指定文件加锁
            FileLock lock = fc.tryLock();
            Thread.sleep(3000);
            lock.release();//释放锁
        }
    }
}

Java的NIO2

Java7对原来的NIO进行了重大改进:

  • 提供了全面的文件IO和文件系统访问支持;
  • 基于异步Channel的IO。

这里先简单介绍一下对文件系统的支持,后续继续学习。

NIO2提供了一下接口和工具类:

  • Path接口:通过和Paths工具类结合使用产生Path对象,可以获取文件根路径、绝对路径、路径数量等;
  • Files工具类:提供了很多静态方法,比如复制文件、一次性读取文件所有行、判断是否为隐藏文件、判断文件大小、遍历文件和子目录、访问文件属性等;
  • FileVisitor接口:代表一个文件访问器,Files工具类使用walkFileTree方法遍历文件和子目录时,都会触发FileVisitor中相应的方法,比如访问目录之前、之后,访问文件时,访问文件失败时;
  • WatchService:监控文件的变化;

NIO和IO的区别

Java的NIO提供了与标准IO不同的工作方式:

  • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,没有被缓存在任何地方,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,因此可以前后移动流中的数据;
  • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。因为NIO将阻塞交给了后台线程执行。而IO是阻塞的;
  • Selectors(选择器):选择器允许一个单独的线程可以监听多个数据通道((网络连接或文件),你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。 

使用场景

NIO

  • 优势在于一个线程管理多个通道;但是数据的处理将会变得复杂;
  • 如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,采用这种;

传统的IO

  • 适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的;
  • 如果需要管理同时打开不太多的连接,这些连接会发送大量的数据;

scu酱油仔
1.3k 声望31 粉丝

没有努力过就没有资格说自己运气不好


下一篇 »
数字全排列