Java NIO
前言
在学习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的内存复制分为四步:
- JVM发出read()系统调用,通过read()系统调用向内核发起读请求;
- 内核向硬件发送读指令,等待读就绪;
- 内核把要读取的数据复制到指定的内核缓存中;
- 操作系统内核将数据复制到用户空间缓冲区,然后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没有大量推行是因为它没有根本上的突破。