Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【JAVA】NIO 如何实现多路复用?

【JAVA】NIO 如何实现多路复用?

作者头像
sidiot
发布于 2023-08-31 05:47:58
发布于 2023-08-31 05:47:58
80800
代码可运行
举报
文章被收录于专栏:技术大杂烩技术大杂烩
运行总次数:0
代码可运行

前言

IO 一直是软件开发中的核心部分之一,伴随着海量数据增长和分布式系统的发展,IO 扩展能力愈发重要。幸运的是,Java 平台 IO 机制经过不断完善,虽然在某些方面仍有不足,但已经在实践中证明了其构建高扩展性应用的能力。

本篇博文的重点是,Java 提供了哪些 IO 方式? NIO 如何实现多路复用?

概述

Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。

第一,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。

第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。  

分析

上面列出的回答是基于一种常见分类方式,即所谓的 BIO、NIO、NIO 2(AIO)。

在实际面试中,从传统 IO 到 NIO、NIO 2,其中有很多地方可以扩展开来,考察点涉及方方面面,比如:

  • 基础 API 功能与设计, InputStream/OutputStream 和 Reader/Writer 的关系和区别。
  • NIO、NIO 2 的基本组成。
  • 给定场景,分别用不同模型实现,分析 BIO、NIO 等模式的设计和实现原理。
  • NIO 提供的高性能数据操作方式是基于什么原理,如何使用?
  • 或者,从开发者的角度来看,你觉得 NIO 自身实现存在哪些问题?有什么改进的想法吗?  

实战

首先,需要澄清一些基本概念:

  • 区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
  • 区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。

不能一概而论认为同步或阻塞就是低效,具体还要看应用和系统特征。

对于 java.io,我们都非常熟悉,这里就从总体上进行一下总结:

  • IO 不仅仅是对文件的操作,网络编程中,比如 Socket 通信,都是典型的 IO 操作目标。
  • 输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
  • 而 Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。
  • BufferedOutputStream 等待缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了 flush。
  • 参考下面这张类图,很多 IO 工具类都实现了 Closeable 接口,因为需要进行资源的释放。比如,打开 FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。利用 Cleaner 或 finalize 机制作为资源释放的最后把关,也是必要的。

1、Java NIO 概览

首先,熟悉一下 NIO 的主要组成部分:

Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。

Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加操作系统底层的一种抽象,这也使得 NIO 得以充分利用现代操作系统底层机制,获得特定场景的性能优化,例如,DMA(Direct Memory Access)等。不同层次的抽象是相互关联的,我们可以通过 Socket 获取 Channel,反之亦然。

Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别。

Charset,提供 Unicode 字符串定义,NIO 也提供了相应的编解码器等,例如,通过下面的方式进行字符串到 ByteBuffer 的转换:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Charset.defaultCharset().encode("Hello world!"));

2、NIO 能解决什么问题?

下面通过一个典型场景,来分析为什么需要 NIO,为什么需要多路复用。

设想,我们需要实现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。使用 java.io 和 java.net 中的同步、阻塞式 API,可以简单实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DemoServer 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 (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                ;
            }
        }
    }
    
    public static void main(String[] args) throws IOException {
        DemoServer server = new DemoServer();
        server.start();
        try (Socket client = new Socket(InetAddress.getLocalHost(), server.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.println("Hello world!");
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 }

其实现要点是:

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

这样,一个简单的 Socket 服务器就被实现出来了。

思考一下,这个解决方案在扩展性方面,可能存在什么潜在问题呢?

大家知道 Java 语言目前的线程实现是比较重量级的,启动或者销毁一个线程是有明显开销的,每个线程都有单独的线程栈等结构,需要占用非常明显的内存,所以,每一个 Client 启动一个线程似乎都有些浪费。

那么,稍微修正一下这个问题,我们引入线程池机制来避免浪费。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
serverSocket = new ServerSocket(0);
executor = Executors.newFixedThreadPool(8);
 while (true) {
    Socket socket = serverSocket.accept();
    RequestHandler requestHandler = new RequestHandler(socket);
    executor.execute(requestHandler);
}

这样做似乎好了很多,通过一个固定大小的线程池,来负责管理工作线程,避免频繁创建、销毁线程的开销,这是我们构建并发服务的典型方式。

如果连接数并不是非常多,只有最多几百个连接的普通应用,这种模式往往可以工作的很好。但是,如果连接数量急剧上升,这种实现方式就无法很好地工作了,因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。

NIO 引入的多路复用机制,提供了另外一种思路:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class NIOServer extends Thread {
    public void run() {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建Selector和Channel
            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            serverSocket.configureBlocking(false);
            // 注册到Selector,并说明关注点
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();// 阻塞等待就绪的Channel,这是关键点之一
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                   // 生产系统中一般会额外进行就绪状态检查
                    sayHelloWorld((ServerSocketChannel) key.channel());
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void sayHelloWorld(ServerSocketChannel server) throws IOException {
        try (SocketChannel client = server.accept();) {          client.write(Charset.defaultCharset().encode("Hello world!"));
        }
    }
   // 省略了与前面类似的main
}

这个非常精简的样例掀开了 NIO 多路复用的面纱,我们可以分析下主要步骤和元素:

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

可以看到,在前面两个样例中,IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。

在 Java 7 引入的 NIO 2 中,又增添了一种额外的异步 IO 模式,利用事件和回调,处理 Accept、Read 等操作。 AIO 实现看起来是类似这样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
AsynchronousServerSocketChannel serverSock =        AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() { //为异步操作指定CompletionHandler回调函数
    @Override
    public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
        serverSock.accept(serverSock, this);
        // 另外一个 write(sock,CompletionHandler{})
        sayHelloWorld(sockChannel, Charset.defaultCharset().encode
                ("Hello World!"));
    }
  // 省略其他路径处理方法...
});

后记

以上就是 【JAVA】NIO 如何实现多路复用? 的所有内容了;

初步对 Java 提供的 IO 机制进行了介绍,概要地分析了传统同步 IO 和 NIO 的主要组成,并根据典型场景,通过不同的 IO 模式进行了实现与拆解。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java 提供了哪些 IO 方式, NIO 如何实现多路复用
Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。
王小明_HIT
2020/05/25
1.9K0
Java核心(五)深入理解BIO、NIO、AIO
导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别;BIO、NIO、AIO 的区别;理解和实现 NIO 操作 Socket 时的多路复用;同时掌握 IO 最底层最核心的操作技巧。
磊哥
2018/12/24
6980
BIO、NIO、IO多路复用模型的演进&Java NIO 网络编程
上文介绍了网络编程的基础知识,并基于 Java 编写了 BIO 的网络编程。我们知道 BIO 模型是存在巨大问题的,比如 C10K 问题,其本质就是因其阻塞原因,导致如果想要承受更多的请求就必须有足够多的线程,但是足够多的线程会带来内存占用问题、CPU上下文切换带来的性能问题,从而造成服务端崩溃的现象。怎么解决这一问题呢?优化呗,所以后面就有了NIO、AIO、IO多路复用。本文将对这几个模型详细说明并基于 Java 编写 NIO。
王二蛋
2024/01/18
7750
阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一锅端
本文会涉及到阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO等几个知识点,知识点虽然不难但经常容易搞混,这次带领大家再回顾一遍。
码农编程进阶笔记
2021/07/20
4050
阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一锅端
Java网络编程与NIO详解2:JAVA NIO 一步步构建IO多路复用的请求模型
本文转载自:htJava网络编程与NIO详解2:JAVA NIO 一步步构建IO多路复用的请求模型
Java技术江湖
2019/11/21
6910
掌握NIO,程序人生
就像新IO为java带来的革新那样,让我们也开启一段新的程序人生。 关键字:NIO,BIO,伪IO,AIO,多路复用选择器,通道,缓冲区,jdk研究,回调函数,高并发 java.nio 概述 历史背景 在java nio出现之前,java网络IO是只有输入输出流操作的基于同步阻塞的Socket编程,这在实际应用中效率低下,因此当时高性能服务器开发领域一直被拥有更接近UNIX操作系统的Channel概念的C++和C长期占据。我们知道现代操作系统的根源都是来自于UNIX系统,它代表了操作系统层面底层
文彬
2018/05/08
1.3K1
Netty序章之BIO NIO AIO演变
Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种NIO框架。而BIO,NIO,AIO更是笔试中要考,面试中要问的技术。也是一个很好的加分项,加分就是加工资,你还在等什么?本章带你细细品味三者的不同! 流程图:
用户1212940
2022/04/13
5380
Netty序章之BIO NIO AIO演变
Java 中 3 种常见的 IO 模型
如下图,应用程序发出一个 read 调用,内核空间需要经历准备数据的几个阶段,准备好之后返回数据给应用程序。期间如果另一个应用程序也需要 read 调用,那么它必须等待;这就是阻塞。
wsuo
2021/06/24
2990
100% 弄明白Java NIO
Java NIO是为了解决高并发请求提出的设计模型,是基于IO多路复用设计出来的。底层又依赖于操作系统的支持(select、poll、epoll)。
公众号 云舒编程
2024/01/25
2270
Netty-nio
channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层
sgr997
2022/11/10
7150
Netty-nio
IO通信模型(三)多路复用IO
从非阻塞同步IO的介绍中可以发现,为每一个接入创建一个线程在请求很多的情况下不那么适用了,因为这会渐渐耗尽服务器的资源,人们也都意识到了这个 问题,因此终于有人发明了IO多路复用。最大的特点就是不需要开那么多的线程和进程。 多路复用IO是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
未读代码
2019/11/04
4390
IO通信模型(三)多路复用IO
Java 的 NIO 是如何工作的?
在这个数据爆炸的时代,有大量的数据在系统中流动,一个应用系统的瓶颈往往都是 IO 瓶颈。传统的 javaIO 模型是 BIO,也就是同步阻塞 IO,数据在写入 OutputStream 或者从 InputStream 读取时,如果没有数据没有读到或写完,线程都会被阻塞,处于等待状态,直到数据读取完成或写入完成。而在网络编程中,每一个客户端连接发出后,服务端都会有一个对应线程来处理请求,服务器线程与并发数成 1:1 关系,然而一个服务器所能处理的线程是有限的,处理高并发时就会有问题。
水货程序员
2018/11/13
1.7K0
Java网络编程——NIO的阻塞IO模式、非阻塞IO模式、IO多路复用模式的使用
NIO虽然称为Non-Blocking IO(非阻塞IO),但它支持阻塞IO、非阻塞IO和IO多路复用模式这几种方式的使用。
DannyHoo
2022/08/07
5240
Java网络编程——NIO的阻塞IO模式、非阻塞IO模式、IO多路复用模式的使用
JAVA网络IO之NIO/BIO
以Linux为例: 第一阶段:调用read读取socket数据,有数据则读取,没数据则等待。 第二阶段:调用read读取socket数据,有数据则读取,没数据则返回-1, errno设置为EAGAIN。 第三阶段:监听socket,有数据则通知。
程序猿川子
2025/02/19
1050
JAVA网络IO之NIO/BIO
Netty - I/O模型之NIO
一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。
小小工匠
2021/08/17
3100
java架构之路-(netty专题)初步认识BIO、NIO、AIO
  本次我们主要来说一下我们的IO阻塞模型,只是不多,但是一定要理解,对于后面理解netty很重要的
小菜的不能再菜
2020/02/21
4570
Java I/O 模型的演进
什么是同步?什么是异步?阻塞和非阻塞又有什么区别?本文先从 Unix 的 I/O 模型讲起,介绍了5种常见的 I/O 模型。而后再引出 Java 的 I/O 模型的演进过程,并用实例说明如何选择合适的 Java I/O 模型来提高系统的并发量和可用性。
九州暮云
2019/08/21
3930
Java I/O 模型的演进
Java进阶(五)Java I/O模型从BIO到NIO和Reactor模式
Java I/O模型 同步 vs. 异步 同步I/O 每个请求必须逐个地被处理,一个请求的处理会导致整个流程的暂时等待,这些事件无法并发地执行。用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行。 异步I/O 多个请求可以并发地执行,一个请求或者任务的执行不会导致整个流程的暂时等待。用户线程发起I/O请求后仍然继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 阻塞 vs. 非阻塞 阻塞 某个请求发出后,由于该请求操作需要的条件不满足,请求操作一直阻塞,
Jason Guo
2018/06/19
7150
BIO NIO AIO演变1 BIO2 NIO3 AIO4 常见面试题5 总结
Block-IO 是一种阻塞同步的通信模式。 常说的Socket IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
JavaEdge
2018/08/10
1.4K0
BIO NIO AIO演变1 BIO2 NIO3  AIO4 常见面试题5  总结
Java NIO
通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。传统的 Server/Client模式如下图所示:
conanma
2021/12/06
4530
相关推荐
Java 提供了哪些 IO 方式, NIO 如何实现多路复用
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验