【Java基础】BIO 与 NIO 的区别


在进入主题之前,先说一下BIO的发展史,

  1. 一开始我们使用的BIO,但是BIO是同步阻塞的,即这个线程没有处理完数据的话,是无法进行其他操作的;
  2. 于是用多线程处理进行了改造,即每个请求开启一个新线程,但是这样又存在一个问题:线程过多(每个线程对象都是占用内存的,32位系统1个线程对象默认最大需要320kb内存,64位系统最大需要1M,如果并发高的话,内存是不够的,会出现内存爆满情况);
  3. 接着又用线程池改造,限定线程的数量,此时又会出现一个问题,比如线程池设置的并发数量为100,这100个线程都是处理大文件,101只是处理几kb的文件,但是要等待好久
  4. 为了解决以上问题,出现了NIO

1. 基础概念

1.1 阻塞与非阻塞

  阻塞和非阻塞是进程访问数据时,数据是否准备就绪的一种处理方式:
  阻塞:当数据没有准备好,进程必须等待缓冲区的数据准备好才能返回做其他事情,否则一直处于等待,无法进行其他事情
  非阻塞:当数据没有准备好,直接返回不会等待,如果数据已经准备好,则直接返回

1.2 同步与异步

  同步和异步是基于应用程序和操作系统处理IO时事件所采用的方式:
  同步:应用程序直接参与IO读写操作
  异步:所有的IO读写操作交给操作系统处理,应用程序只需要等待通知


2. BIO 与 NIO

  首先先明白连接请求和IO请求,客户端和服务端进行通信时,首先服务端会监听到有一个客户端请求,为其创建创建通信套接字(Socket),此时可能只是建立了一个连接,不做什么的数据传输即IO操作,所以要明白 连接请求和IO请求不是必然的关系,即有连接 一定有IO

2.1 BIO

2.1.1概念

  同步阻塞IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务端为其建立一个线程进行处理,如果这个线程不做任何事情,这个线程就一直空闲,但也不会做其他事情,这样造成了线程的浪费

2.1.2 实现机制

  面向流,即I/O系统以流的形式处理数据,一次一个字节的处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。

在这里插入图片描述
在这里插入图片描述

2.1.3 工作原理

  1. 由一个专门的线程处理所有的IO事件,并负责分发
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件
  3. 线程通讯:线程之间同过wait,notify等方式通讯,保证每次上下文切换都是有意义的,减少五位的线程切换

在这里插入图片描述

2.2 NIO

2.2.1 概念

  同步非阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时,都会注册到多路复用器上,多路复用器轮询到有IO请求时,才启动一个线程处理

2.2.1 实现机制

  面向块,即面向块,即I/O系统以块的形式处理数据,一次一个数据块,线程这样比流快,但是面向块的I/O不如面向流I/O优雅和简单
在这里插入图片描述
在这里插入图片描述

2.2.2 为什么使用NIO

  目的是为了让 Java 程序员可以实现高速 I/O ,NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度

2.2.3 三大主要组件

2.2.3.1 通道 Channel

  Channel和IO中的stream流差不多是一个等级的。只不过stream是单向的,而Channel是双向的,即可以用来读操作,也可用来写操作。使用NIO读取数据分为三个步骤:

  1. 从FileInputStream获取Channel
  2. 创建Buffer
  3. 将数据从Channel读取到Buffer中
2.2.3.2 选择器 Selector

  首先先说一下为了解决由于线程数量的增加会增大服务器的开销采用线程池模型存在的问题,如果线程池有200个线程,而有200个用户在进行大文件下载,会导致第201个用户的请求无法及时处理,即使第201个用户知识想请求一个几KB大小的页面。NIO采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,相反时注册感兴趣的特定I/O事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各种I/O事件的地方,而且当那些事件发生时,就时这个对象告诉我们所发生的事件:
在这里插入图片描述
  从图中可看出,当有读或写任何注册事件发生时,可以从Selector中获得对应的SelectKey,同时从SelectKey中可以找到发生的事件和该事件所发生的具体的SelecttableChannel,以获得客户端发送过来的数据。使用NIO中非阻塞I/O编写服务器处理程序,大体分为三个步骤:

  1. 向Selector对象注册感兴趣的事件
  2. 从Selector中获得感兴趣的事件
  3. 根据不同的事件进行相应的处理
2.2.3.3 缓冲区 Buffer

  NIO中的关键Buffer实现有:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer,其中还有一个MappedByteBuffer。缓冲区实际上是一个容器对象,更直接的说,其实就是一个特殊的数组,缓冲区对象内置了一些机制,能跟踪和记录缓冲区的状态变化。在NIO中,所有数据都是用缓冲区处理的。在读数据时,先直接读到缓冲区;在写数据时,先写入缓冲区;但是IO中,所有的数据都是直接写入或将数据读到Stream对象中。
在这里插入图片描述

2.2.4 有关缓冲的一些概念或技术

  • 缓冲区分片:在现有缓冲区对象来创建一个子缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上数据是共享的。通缓冲区的slice()方法创建一个子缓冲区。
  • 直接缓冲区和非直接缓冲区区别:https://blog.csdn.net/weixin_29218537/article/details/79659303https://www.cnblogs.com/androidsuperman/p/7083049.html
  • 只读缓冲区:可以读取,但不能写入数据。通过缓冲区的asReadOnlyBuffer()方法将常规缓冲区转换为只读缓冲区。原缓冲区的内容发生变化,只读缓冲区也会随之发生变化。
  • 内存映射文件IO:也是一种读和写文件数据的方法,比常规的基于流或基于通道的IO快的多。通过将硬盘上的文件位置与进程逻辑地址空间中的一块大小相同区域一一对应,当要访问内存中一段数据时,,转换为访问文件的某一段数据。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。更多详解参见:https://blog.csdn.net/u010688637/article/details/79568501

3. 面向流和面向缓冲

  Java NIO 和 IO之间最大的区别是:IO是面向流的,NIO是面向缓冲的。

  • Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有的字节,他们没有被缓冲存在任何地方,且
    它不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要将它缓冲到一个缓冲区;
  • Java NIO是将数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,增加了处理过程中的灵活性,但是需要检查缓冲区中是否包含您需要处理的数据,并且确保更多的数据缓冲读入缓冲区时,不要覆盖缓冲区里尚未处理的数据

4. 为什么NIO比BIO效率高

  https://blog.csdn.net/wy0123/article/details/79382761

5.总结

在这里插入图片描述
  最后以一张图总结NIO的运行机制,将这种运行机制比作成饭店的一个场景,Channel相当于厨师,Selector相当于取号的一个机器,Thread相当于上菜人员,厨师(Channel)要给客人做菜时,取机器(Selector)那取一下菜单开始准备做菜(注册),上菜人员(Thread)只需到制定位置看哪桌的菜好了就行(监控),此时如果厨师做好了菜,就会把该桌号报给机器说几号桌的菜好了,上菜人员将做好的菜给顾客,顾客就可以吃饭了(ThreadPool就new一个线程,对数据开始操作,即开始服务了)