JavaIO流及NIO如何实现多路复用

初学Java的时候大家都会接触到各种各样的IO流,我们可以知道,IO流的扩展方式是多种多样的,这篇文章,就会主要的来介绍一下IO流。

简介

Java IO流方式多种多样,我们可以从IO抽象模型和交互方式,进行简单的划分。

第一,传统的java.io包,完全基于流模型实现,提供了一些我们熟知的IO功能,比如File抽象、输入输出流等等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流的时候,在这两个操作结束之前,线程呈阻塞状态,但是呈线性,所以有一定规律。

java.io包的特点是,代码简单,原理移动,可扩展性低,性能差。

第二,为了在保证原本功能的情况下提升性能,所以java14引入了NIO框架,提供了Channel,Selector,Buffer等新的抽象类,构建多路复用,同步且非阻塞,且提高了性能。

第三,在java17中,NIO又进行了更新,也就是NIO2,引入了异步非阻塞IO,也就是我们听过的AIO(Asynchronous IO) 。
异步IO操作基于事件和回调,可以简单的理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成之后,操作系统会通知相应的线程在后续工作。

在面试中,一般面试方向是这样的:

  • java.io的基础包用法,像Input/OutStream,Reader/Writer
  • NIO和NIO2的基本组成
  • 给定场景,用不同模型实现,分析BIO和NIO等模式的设计和实现原理。
  • NIO提供的高性能数据操作方式是基于什么原理,如何使用
  • NIO自身的问题,如何改进

细节上的东西在下一篇文章介绍,这篇文章简要的进行概述

扩展

首先区分一下同步和异步:

同步:有序运行,后续任务等待当前调用返回,才会执行下一步
异步:不需要等待当前调用返回,通常依赖事件、回调等机制实现任务间次序关系

然后区分一下阻塞与非阻塞:

阻塞:当阻塞的时候,当前线程会处于阻塞状态,无法从事其他任务,只有当任务就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取,写入操作完成
非阻塞:不管IO是否结束,直接返回,操作在后台自己处理。

不能一概而论,阻塞和同步就是低效,要看实际应用场景

对于java.io,我们都非常熟悉,但是还有很多底层和细节方面的问题需要仔细的探究,以下是必须要了解到的知识点:

  1. IO不仅仅是对文件的操作,更包括了Socket。
  2. 输入输出流是用于读取或写入字节的,例如操作图片文件。
  3. Reader/Writer 主要用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字符,不管是网络通信还是文件读取,Reader/Writer相当于构建了应用逻辑与原始数据之间的桥梁。
  4. BufferedOutputStream这种带缓冲区的IO读写可以大大提升读写的效率,但是最后需要flush
  5. 很多IO工具类都实现了Closeable接口,因为要进行资源的释放。比如,打开FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用try-with-resource、try-finally等机制保证FileInputStream被明确关闭,进而响应的文件描述符也会失效,否则资源将无法释放。

JavaIO流及NIO如何实现多路复用_第1张图片

  1. Java NIO概述

首先,描述一下NIO的主要组成:

  • Buffer :高效的数据容器,除了布尔类型,所有原始数据类型都有响应的Buffer来实现
  • Channel:批量操作IO的抽象
  • Selector:可以通过检测Channel的状态,达到单线程管理Channel
  • Charset:提供Unicode字符串定义
  1. NIO能解决什么问题

我们要实现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。

package jike_Time;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerNIO extends Thread{
    private ServerSocket serverSocket;
    public int getPort() {
        return serverSocket.getLocalPort();
    }

    public void run() {
        try {
            serverSocket = new ServerSocket(0);
            while (true) {
                Socket socket = serverSocket.accept();
                RequestHandler requestHandler = new RequestHandler(socket);
                requestHandler.start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        ServerNIO serverNIO = new ServerNIO();
        serverNIO.start();
        try(Socket client = new Socket(InetAddress.getLocalHost(), serverNIO.getPort())){
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
}

//简化实现
class RequestHandler extends Thread {
    private Socket socket;

    RequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
            out.print("Hello world");
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其实现要点在于:

  • 服务器端启动ServerSocket,端口0表示总动绑定一个空闲端口
  • 调用accept方法,阻塞等待客户端连接
  • 利用Socket模拟了一个简单的客户端,只连接、读取、打印
  • 当连接建立之后,启动一个单线程负责恢复客户端请求

这样一个Socket服务器就实现了,那么会存在什么潜在的问题呢?

我们都知道java的线程是比较重量级的,所以启动,关闭,销毁一个进程是有一定开销的,每一个线程都有单独的线程栈,需要很明显的内存支持,所以我们可以引入线程池。

ExecutorService executorService = Executors.newFixedThreadPool(8);

这是一个经典的并发服务,可以参考下图来看:

JavaIO流及NIO如何实现多路复用_第2张图片

当我们用普通IO进行这种低并发的时候,效果还是可以的,但是当连接数量急剧上升的时候,就会出现同步阻塞的低扩展劣势,而NIO解决了这样的问题。

  • 首先,通过Selector.open()创建一个Selector,作为类似调度员的角色。
  • 然后创建一个ServerSocketChannel,并且向Selector注册,通过指定SeletionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。
    注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出异常
  • Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒。
  • 在数据的方法中,通过SocketChannel和Buffer进行数据操作,在本例中是发送了一串String。

我们可以得出以下结论:
IO都是同步阻塞,所以需要多线程,NIO则是单线程轮询,提高应用的扩展能力。

JavaIO流及NIO如何实现多路复用_第3张图片

你可能感兴趣的:(JAVA核心知识)