Java面试总结之IO、NIO篇-你还在傻傻的分不清楚IO与NIO吗


前言

  • 鉴于现在业界大多数情况下不怎么问 AIO (对于我而言), 这篇面试总结仅仅只是关于 BIONIO

阻塞与非阻塞

  • 在进程访问数据的时候, 数据是否准备就绪的一种处理方式, 当数据没有准备就绪时:
    • Block IO: 是一种传统的一种 I/O , 也就是说在 read/write 的过程中会发生阻塞的现象, 往往是需要等待缓冲区中的数据贝准好之后才处理其他的事情, 否则会一直阻塞. 当用户线程发出 I/O 请求之后, 内核会查看缓冲区中的数据是否准备好, 如果没有准备好, 用户线程就会等待数据准备好并且进入阻塞状态. 用户线程就放弃 CPU 控制权. 在数据准备好之后, 内核就会将缓冲区中的数据拷贝到用户线程, 并返回结果给用户线程, 用户线程才会解除阻塞状态.
    • None-Block I/O: 当用户线程访问数据缓冲区时, 无论数据是否准备好都直接得到结果, 不会进行等待, 如果是一个 Error 情况下, 就直到数据并没有准备好, 将再次访问数据缓缓冲区, 一旦内核中的数据准备就绪, 并且再次接收到用户线程的请求时, 就会把数据拷贝到用户线程并返回, 在 None-Block I/O 中, 用户线程需要不断地询问内核数据是否就绪, 会一直占用着 CPU 资源

同步与异步

  • 同步与异步都是基于 应用程序OS 处理 I/O 事件采用的方式,
    • 同步 : 是应用程序需要参与 I/O 的读写操作, 并且在处理 I/O 事件的时候, 必须阻塞在某个方法上面等待内核中的数据准备就绪, 在这个情况下, 无论是 read/write 操作, 我们就不能进行下面的操作
    • 异步 : 所有的 I/O 的读写操作交给 OS 处理, 应用程序只是需要等待通知, 在这个情况下, 我们可以去做其他事情, 并不需要去阻塞等待 I/O 完成操作, 当 I/O 完成操作之后会给我们一个通知. 【例: Ajax】, 从内核的角度, 当收到了一个异步读之后就会马上返回, 说明读请求已经发送成功, 所以不会对用户线程产生阻塞. 接着, 内核会等待数据准备就绪, 然后将数据拷贝到用户线程, 也就是说用户线程完全不需要知道的整个 IO 操作是怎么进行, 只是先发起一个请求, 在接收到内核返回的成功通知的时候, 就表示 I/O 操作已经完成.

JAVA BIO 包

在这里插入图片描述

JAVA NIO

NIO 网络模型图

在这里插入图片描述

NIO 核心对象之 Channel

  • Channel 可以用来进行读与写的操作, read/write 操作的数据都是通过 Buffer 对象进行处理, 对读操作的时候会把数据读入缓冲区, 而写操作的时候会从缓冲区获取该数据

Channel 主要的实现类

在这里插入图片描述

扫描二维码关注公众号,回复: 9746139 查看本文章

NIO 核心对象之 Buffer

  • Buffer 是一个容器, 是一个连续数组, 在 NIO 中, 所有的读/写数据操作都是通过 Buffer

Buffer 读写操作图

在这里插入图片描述

Buffer 的主要实现类

在这里插入图片描述

Buffer 实现原理

  • Buffer 是一个特殊的数组, 其内置了一些机制, 能够跟踪和记录缓冲区中的状态变化情况, 如果使用 Buffer#read()/flip()/get()/clear() 方法时, 都会引起缓冲区状态的变化
	// Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

Buffer 状态值测试代码

package com.cj.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @Author: CaoJun
 * @Create: 2020-03-11 06:03
 **/
public class BufferTest {

    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream(new File("C:\\Users\\john\\Desktop\\nio\\bufferTest.txt"));
        // 获取文件操作管道
        FileChannel fileChannel = fis.getChannel();
        // 分配大小为 10 的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        printBufferStatus("初始化缓冲区大小 ", byteBuffer);

        // 读取缓冲区
        fileChannel.read(byteBuffer);
        printBufferStatus("调用 read() 方法", byteBuffer);

        // 锁定操作的范围
        byteBuffer.flip();
        printBufferStatus("调用 flip() 方法", byteBuffer);

        // 判断文件中是否有数据
        while (byteBuffer.remaining() > 0) {
            byteBuffer.get();
        }
        printBufferStatus("调用 get()  方法", byteBuffer);

        // 清除缓冲区中的数据
        byteBuffer.clear();
        printBufferStatus("调用 clear() 方法", byteBuffer);

        // 关闭管道
        fileChannel.close();

    }

    /**
     * 打印缓冲区状态
     */
    private static void printBufferStatus(String step, Buffer buffer) {
        System.out.print(step + " : ");
        //容量,数组大小
        System.out.print("capacity: " + buffer.capacity() + ", ");
        //当前操作数据所在的位置,也可以叫做游标
        System.out.print("position: " + buffer.position() + ", ");
        //锁定值,flip,数据操作范围索引只能在 position - limit 之间
        System.out.println("limit: " + buffer.limit() + "\r\n");
    }

}

Buffer 状态值测试结果

在这里插入图片描述

bufferTest.txt 文件中的数据

在这里插入图片描述

Buffer 状态值变动图

在这里插入图片描述

NIO 核心对象之 Selector

  • NIO 中的 None-Block I/O 采用基于 Reactor 模式的工作方式, 在 I/O 调用时不会被阻塞. SelectorNIO 的核心类, Selector 能够检测多个注册的通道上是否有事件发生, 如果有事件发生时, 就获取事件并对每个事件都进行相应的响应处理, 这样就可以只用一个线程就可以管理过个通道. 所以只有在连接读写事件的操作时, 才会调用方法进行读写操作, 这样就大大地减少了系统的开销, 并且不用为每一个连接创建线程, 不用维护多个线程并避免了多线程之间的上下文切换开销.
  • 在下图中无论读/写等任何注册的时间发生的时候, 可以从 Selector 中获得相应的 SelectionKey, 同时从 SelectionKey 中可以找到发生的时间和该时间所发生的具体的 SelectableChannel去获取客户端发送过来的数据
  • 使用 NIO 中的非阻塞 I/O 编写服务器处理程序分为以下几个步骤
    • Selector 对象注册感兴趣的事件
    • Selector 对象中获取感兴趣的事件
    • 根据不同的事件进行相应的处理

在这里插入图片描述

总结 BIO 与 NIO 对比

  • 面向流和面向缓冲 :
    • BIO 是面向流的, 意味着每次从六中读 1-n 个字节, 知道读取出所有的字节, 读取出的字节并没有被缓存, 也不能前后移动流中的数据, 如果需要前后移动从流中读取的数据, 就需要先将它们缓存到一个缓冲区中,
    • NIO 是面向缓冲的, 意味着每次读取到的数据先到缓冲区中, 当需要的时候可以在缓冲区中进行前后移动, 这样就增加了处理过程中的灵活性
  • 阻塞和非阻塞 :
    • BIO 中的流是阻塞的, 意味着用户线程每次进行读/写操作的时候都会被阻塞, 直到有一些数据被读取, 或者完成数据的写书, 与此同时该用户线程不能做其他操作, 如果没有可用数据的情况下, 就什么都不会获取, 而不是保证线程阻塞, 所以直到数据能在可以读取的状态下之前, 该线程还可以去做其他的操作.
    • NIO 中的通道是非阻塞的, 一个用户线程请求写入数据到 Channel 中, 但是不需要等待它完全写入, 该用户线程还可以去做其他的操作. 用户线程通常将 None-Block I/O 的空闲时间用于在其他通道上执行 I/O 操作, 所以一个单独的线程就可以管理 n 个输入/输出通道
      选择器 :
    • Java NIO 中的 Selector 可以一个线程去监视 n 个通道, 并且可以注册多个 Channel 同时使用一个 Selector, 然后使用一个单独的线程来选择通道, 这些通道里已经有可以处理的输入, 或者选择已经准备写入的通道, 这种选择机制会使一个单独的线程很容易地管理 n 个通道
IO模型 BIO NIO
通信 面向流 面向缓冲
处理 阻塞IO (Multi Thread) 非阻塞IO (Reactor)
触发 选择器 (Round)

  • Okay, BIO and NIO are finished right now. Nothing is impossible as long as you are willling to do it,
发布了23 篇原创文章 · 获赞 3 · 访问量 1156

猜你喜欢

转载自blog.csdn.net/weixin_38251871/article/details/104788614