Java从BIO到NIO的演进过程

1. 前言

关于Java的IO这一块,说实话一直是让人又爱又恨的存在。大部分程序员实际工作中很少会接触文件操作相关的,即使用到了,百度随便找个工具类,能解决问题就完事。

然而你真正打算去学习IO流的时候,网上的文章上来就是一张这种图片,虽说总结得很好,但直接让人失去了看下去的欲望。


事实上,所有系统都是慢慢演进出来的,今天就和大家一起探讨一下,JavaIO的演进过程。



2. BIO的演进史

首先计算机和外界进行信息的输入和输出交互,用的是比特流
那么很容易就能想到IO流名字的由来了,就是比喻输入输出的数据像流一样。

我们可以这么认为,任何外部设备与内存之间输入输出的操作,都是需要IO流来完成的,而这里的IO流,指的就是比特流(或者称字节流)

2.1 从内存到外界

假如我们需要把内存里的数据输出或者输出到外界,你会如何思考?

这其中有两点是我们需要关注的:

  1. 输入还是输出?
  2. 外界可能会是什么?

于是很简单,Java为我们提供了InputStream、OutputStream两个顶层抽象类,用来表示操作所有的输入输出。那么如果我们需要对外界进行输入输出时,只需要提供其子类即可。

假如这时候我要操作文件的时候,就需要具体的对文件系统操作的IO实现类,于是我们需要学习
FileInputStream、FileOutputStream


假如我们以后还需要操作机器人,那么或许就会再来个RobotInputStream和RobotOutputStream,这些新的需求也就都可以继承这个体系

2.2 如何高效

有了文件IO字节流之后,我们会发现原始的字节流对象用起来没那么高效。
因为每个读或写请求都由底层操作系统处理,这些请求往往会触发磁盘访问、网络活动或其他一些相对昂贵的操作。

怎么提升读写效率?

不带缓冲区的流对象,只能一个字节一个字节的读,每次都调用底层的操作系统API,非常低效,而带缓冲区的流对象,可以一次读一个缓冲区,缓冲区空了才去调用一次底层API,这就能大大提高效率。所以又有了

BufferedInputStream、BufferedOutputSteam


2.3 更加方便

效率是提升上去了,但是开发人员在使用的时候,常常是希望能将读取到的数据转换成人类能阅读的信息,而不是一大串“001001010”之类的字节流。

于是Java又为我们提供了两个转换流:InputStreamReader和OutputStreamWriter。这两个类的作用分别是把字节流转成字符流,把字符流转成字节流。但是这两个流需要套在现成的字节流上才能使用。

再进一步呢?我们再省去创建字节流,再套上转换流的步骤,于是Java又提供了FileReader和FileWriter这一类工具类。

2.4 小结

至此我们已经能发现其中的规律,无论怎么千变万化,BIO始终在基于输入输出进行演进,针对某些情况为开发者提供更多可用的工具类。

3. NIO的由来

在传统 I/O(BIO)中,InputStream的read()是一个while循环操作,它会一直等待数据读取,直到数据就绪才会返回。

这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态
在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。

但在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。

形象一点呢,就是你花钱招了一堆程序员进来,但是这时候还没有活干,于是大部分程序员就开始摸鱼。

针对这类问题,操作系统自然也做了相应优化,比如说多路复用模型,又比如说零拷贝。而Java也是顺应时代,根据操作系统的优化提供了相应的NEW IO流,所以被称作为NIO。

3.1 Selector

上面说到,由于阻塞问题的存在,BIO在面对大量连接请求时的性能很低。因为每当一个请求到来,就需要创建一个对应的线程去处理,即使一些程序员别出心裁想出了使用线程池来优化,但仍然心有余而力不足。

于是Java为我们提供了Selector,我不需要一个线程对应一个请求,我只需要一个线程去不断轮询所有请求,然后处理活跃的请求即可。

我们不需要知道Selector具体是怎么实现的,也不需要去死磕Selector的方法和源码。我们只需要理解,Selector实际上就是一个轮询的线程,如果有发生监听事件,就可以进行 I/O 操作。

但是问题来了,之前的BIO似乎并不支持这些事件,于是Java需要提供一个能够配合Selector来使用的API,而这个类,就是Channel。

3.2 Channel

在操作系统中对IO设备的控制方式一共有四种,按时间线依次是轮询、中断、DMA、和通道方式。

轮询和中断就不说了,DMA比起中断方式已经显著减少了CPU的干预

但是CPU每发出一条IO指令,只能去读写一个连续的数据块,当要读多个数据块并存放到不同的内存区域中去,CPU需要发送多条IO指令及进行多次中断。

IO通道方式是DMA方式的发展,把对一个数据块的干预减少为对一组数据块的干预。而Java的Channel,就是对这种形式的具体实现。

我们可以理解为,Java通过Channel为我们在程序和操作系统间架了一座桥,数据可以通过这座桥进行输入或者输出,而这座桥则是基于IO通道方式打造的。

所以我们要如何理解Java的Channel呢?实际上它就是顺应了操作系统优化下,为多路复用提供的一个API。

3.3 Buffer

在之前BIO的发展中已经提到,缓冲区对性能提升的重要性。所以Channel的使用需要配合Buffer,这也很容易理解,读写数据需要一个中转地。即使不是NIO,我们也可以在BIO中看到buffer的思想。

此外,对于NIO来说,缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般来说可以减少一次系统空间到用户空间的拷贝,这就是零拷贝的应用。但Buffer创建和销毁的成本更高,更不宜维护。


4 总结

之前在学习一些知识的时候,总是为了看源码而去看一些源码,实际上大部分看了就忘,真正学到的东西少之又少。学习知识应该要做到知其然,知其所以然。而不是为了应付草草了事。

如有错误,还请指正!

参考文章:
https://www.zhihu.com/question/67535292
https://tech.meituan.com/2016/11/04/nio.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值