Java NIO

  • 前言
  • BIO
  • 字节流
  • 字符流
  • NIO
  • 为什么需要NIO
  • BIO vs NIO
  • NIO中的组件以及是如何提高性能的
  • Channel(通道)
  • Buffer(缓冲区)
  • 技巧:利用DirectBuffer减少内存复制
  • Selector(选择区)
  • AIO
  • 总结


前言

在学习JavaSE的时候,大部分人学习的IO都是基于流的BIO,叫做阻塞io。BIO的早期处理文件的方式是边读文件边处理数据,在后期引入了缓冲块流,将文件一次性读入内存再进行操作。

而在jdk1.4中加入了nio的概念,被称为new i/o或者non-block i/o,它是面向Buffer的传输流,Buffer可以将文件一次性读入内存在做后续操作。

在jdk1.7中推出了 nio2 , 提出了操作系统层面实现的异步I/O。

今天就来进行Java的I/O比较。

BIO

Java中的BIO是基于流的IO,可以分为字符流和字节流。流是对字节序列的抽象,流有流动的方向,所以便有输入流和输出流的概念。通常可以从中读取一个字节序列的对象称为输入流,能够向其写入一个字节序列的对象称为输出流。

  • 读取字节序列 ==》 输入流
  • 写入字节序列 ==》 输出流

字节流

Java中的字节流处理的最基本单位是单个字节,一般是用来处理二进制数据。

其中最基本的字节流类是InputStream和OutputStream,代表了最顶层的字节输入流和字节输出流。

InputStream类与OutputStream类的最核心方法便是read方法与write方法。

public abstract int read() throws IOException;

该抽象方法的功能是从字节流中读取一个字节,若到了末位便返回 -1 ,否则返回读取到的字节。
其会一直阻塞直到返回的字节或者-1 。其默认不支持缓存,那么每调用一次 read 方法便会进行一次请求操作系统读取字节,效率较低。

public abstract void write(int b) throws IOException;

其他的重载write方法最后也是调用了该抽象方法。并且与读方法一样,也是一个一个写入。

我们可以很明显的知道,这样子效率极其低下,如果我们需要使用BIO,并且还想要效率变高,那么我们应该使用BufferedInputStream。

字符流

Java中字符流处理的最基本单位是Unicode码元(2字节),通常进行文本数据处理。Java中的String类便是将字符串以Unicode编码之后存储在内存中,而存储在硬盘中时便不是如此,因为硬盘中的文件存储有各种各样的编码方式。而字符流是如何运行的呢?

  • 输出字符流(内存到其他地方):将已经通过Unicode编码的字符序列转为指定编码方式下的字节序列,然后写入文件。
  • 输入字符流(其他地方到内存):将要读取的字节序列按指定编码方式解码为对应的Unicode字符序列存入内存。

NIO

为什么需要NIO

一般来说,一个新的技术的产生应该是迫切的需要解决某些问题。NIO也不例外,NIO的出现便是为了解决传统I/O的性能问题。

BIO vs NIO

BIO

NIO

面向 (最大区别)

面向流

面向Buffer

是否阻塞

读写时阻塞

非阻塞

内存复制

进行多次内存复制

内存复制次数低

面向: 关于BIO,之前已经写了,他是面向流的IO,有输入流和输出流两种;关于NIO,他是面向Buffer(缓冲区),传输效率变高。

是否阻塞: 关于BIO,我们知道,在BIO的读写时候,线程会直接阻塞,无法进行其他操作;关于NIO,NIO有个组件叫做Selector(选择区),是基于事件驱动实现的,也就是说,我们可以在Selector上注册不同的事件,然后Selector会不断轮询注册在其上的Channel,若发生监听操作,那么就Channel处于就绪状态,进行IO操作。

内存复制:
BIO的内存复制分为四步:

  1. JVM发出read()系统调用,通过read()系统调用向内核发起读请求;
  2. 内核向硬件发送读指令,等待读就绪;
  3. 内核把要读取的数据复制到指定的内核缓存中;
  4. 操作系统内核将数据复制到用户空间缓冲区,然后read()系统调用返回。

这个过程中,数据先从外部设备复制到内核空间,再从内核空间到用户空间,进行了两次内存复制,降低IO性能。

而NIO只需要进行一次内存复制。

NIO中的组件以及是如何提高性能的

NIO中的核心组件有:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择区)
Channel(通道)

Channel,国内通常翻译成通道,与流相当于是同一个等级的对象。他是如何提高性能的呢?

首先我们可以知道,最刚刚开始,应用程序调用操作系统I/O接口的时候,是由CPU完成分配,那么在大量I/O请求发送时,CPU的消耗巨大。之后操作系统引入了DMA(直接存储器存储),内核空间与硬盘直接的存取由其负责,但是DMA还是需要向CPU请求权限,且需要借助DMA总线完成数据复制,总线一多容易造成冲突。

而Channel有自己的处理器,可以完成内核空间与硬盘直接的I/O操作,在NIO中,读取和写入都要通过Channel。

Buffer(缓冲区)

Buffer可以将文件一次性读入内存在做后续操作,而流的话需要边读边处理。

后面BIO也有待缓冲区的流,但是其性能难以媲美NIO。

技巧:利用DirectBuffer减少内存复制

数据若要输出到外部设备,那么必须先从用户空间复制到内核空间,在复制到输出设备。

但是DirectBuffer可以直接开辟物理内存,而不是像普通Buffer一样分配JVM内存,这样就可以直接将其从内核空间复制到外部设备,减少数据拷贝。

Tips:但是需要注意一点就是,我们都知道JVM会自动进行GC,但是由于DirectBuffer申请的是物理内存,那么销毁和创建的代价非常高昂,其是通过DirectBuffer包装类被回收时,通过Java Reference 机制释放内存。

Selector(选择区)

Channel和Selector是NIO实现非阻塞的原因。

在BIO中,使用带有Buffered的I/O流依然会存在阻塞问题。而阻塞问题,才是BIO的最大的弊端。我们可以回忆起学习JavaSE的Socket时,我们要进行死循环,一直阻塞接收请求,直到下面三种情况任意一种:

  • 有数据可读;
  • 连接释放;
  • 空指针或者I/O异常

Selector是基于事件驱动的,一个线程使用一个Selector,轮询监听多个Channel上的事件,在注册的时候指定通道为非阻塞。

AIO

关于AIO,很多人会疑惑,现在很多的框架与组件都是基于NIO的,为什么没有使用更新的AIO呢。

原因是因为AIO中并没有真正使用操作系统锁提供的异步I/O,本质还是同步非阻塞I/O。所以大多数还是使用NIO。

总结

I/O主要是基于操作系统的接口进行封装,而关于NIO主要是从阻塞、缓冲、降低内存复制次数三个方面进行优化,而AIO没有大量推行是因为它没有根本上的突破。