从BIO到NIO:Java IO的进化之路

引言

在 Java 编程的世界里,输入输出(I/O)操作是基石般的存在,从文件的读取写入,到网络通信的数据传输,I/O 操作贯穿于各种应用程序的核心。BIO(Blocking I/O,阻塞式 I/O)作为 Java 早期的 I/O 模型,以其简单直观的编程方式,在早期的 Java 应用开发中扮演了重要角色 ,为开发者提供了基础的 I/O 能力。但随着互联网应用的快速发展,尤其是在高并发场景下,BIO 的局限性逐渐暴露,如线程资源消耗大、I/O 操作阻塞导致效率低下等问题,难以满足日益增长的性能需求。

为了突破 BIO 的困境,NIO(New I/O 或 Non - Blocking I/O,新 I/O 或非阻塞 I/O)应运而生。NIO 自 Java 1.4 版本引入后,带来了全新的 I/O 编程理念和方式,它通过 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)等核心组件,构建了一种基于事件驱动的非阻塞 I/O 模型,极大地提升了 I/O 操作的效率和并发处理能力,成为 Java I/O 领域的一次重大变革。

本文将深入探讨 BIO 到 NIO 的演变历程,从背景历史、功能点、业务场景、底层原理等多个维度进行剖析,详细介绍其实现原理,并通过 Java 代码给出至少三个不同的样例,帮助读者全面理解和掌握这两种 I/O 模型的精髓,以及它们在不同场景下的应用。

一、BIO 的诞生与发展

1.1 计算机 IO 的基础概念

在计算机系统中,I/O(Input/Output,输入 / 输出)是指计算机与外部世界进行数据交互的过程。计算机通过输入设备(如键盘、鼠标、摄像头等)接收外部数据,再将处理后的数据通过输出设备(如显示器、打印机、扬声器等)输出到外部 。在这个过程中,数据在内存和外部设备之间流动,而 I/O 操作就是负责管理和控制这种数据流动的机制。

I/O 操作涉及到计算机硬件和软件的多个层面。从硬件角度看,I/O 设备通过设备控制器与计算机的总线相连,设备控制器负责管理设备的具体操作,如数据传输、设备状态监测等。从软件角度看,操作系统提供了设备驱动程序,用于与设备控制器进行通信,使得应用程序能够通过操作系统提供的接口来访问 I/O 设备。

1.2 BIO 的初步实现

BIO(Blocking I/O,同步阻塞 I/O)是 Java 早期的 I/O 模型,它基于流(Stream)的概念来实现数据的读写操作。在 BIO 中,当进行 I/O 操作时,线程会被阻塞,直到操作完成。例如,使用InputStream从文件或网络连接中读取数据时,线程会一直等待,直到有数据可读或读取操作完成;使用OutputStream向文件或网络连接中写入数据时,线程会一直等待,直到数据被完全写入。

以从文件中读取数据为例,Java 代码如下:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class BIOExample {public static void main(String[] args) {try (InputStream inputStream = new FileInputStream("example.txt")) {int data;while ((data = inputStream.read())!= -1) {System.out.print((char) data);}} catch (IOException e) {e.printStackTrace();}}
}

在上述代码中,FileInputStream是InputStream的子类,用于从文件中读取数据。read()方法会阻塞线程,直到读取到一个字节的数据或到达文件末尾(返回 -1)。每次读取一个字节,然后将其转换为字符并打印出来。

1.3 BIO 的优化与工具类扩展

为了提升 BIO 的读写效率,Java 引入了缓冲区(Buffer)的概念。通过使用BufferedInputStream和BufferedOutputStream,可以减少系统 I/O 调用的次数,从而提高性能。BufferedInputStream内部维护了一个缓冲区,当读取数据时,它会一次性从底层输入流中读取多个字节到缓冲区中,然后从缓冲区中返回数据给应用程序。当缓冲区中的数据耗尽时,它会再次从底层输入流中读取数据到缓冲区。BufferedOutputStream的工作原理类似,它会将数据先写入缓冲区,当缓冲区满或调用flush()方法时,才将缓冲区中的数据一次性写入到底层输出流中。

以下是使用BufferedInputStream和BufferedOutputStream进行文件复制的示例代码:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedIOExample {public static void main(String[] args) {try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("target.txt"))) {int data;while ((data = bis.read())!= -1) {bos.write(data);}} catch (IOException e) {e.printStackTrace();}}
}

此外,为了方便字符数据的读写,Java 还提供了转换流InputStreamReader和OutputStreamWriter,它们可以将字节流转换为字符流,并指定字符编码。例如:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class ConvertStreamExample {public static void main(String[] args) {try (InputStreamReader isr = new InputStreamReader(new FileInputStream("source.txt"), "UTF-8");OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("target.txt"), "UTF-8")) {int data;while ((data = isr.read())!= -1) {osw.write(data);}} catch (IOException e) {e.printStackTrace();}}
}

进一步地,Java 还提供了更便捷的字符流操作类FileReader和FileWriter,它们默认使用平台的默认字符编码,简化了字符文件的读写操作。例如:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileReaderWriterExample {public static void main(String[] args) {try (FileReader fr = new FileReader("source.txt");FileWriter fw = new FileWriter("target.txt")) {int data;while ((data = fr.read())!= -1) {fw.write(data);}} catch (IOException e) {e.printStackTrace();}}
}

1.4 BIO 的特点与局限性

BIO 在处理少量连接请求时,具有响应速度高、编程简单等优势。因为每个连接由一个独立的线程处理,代码逻辑清晰,开发人员容易理解和实现。例如,在一些简单的客户端 - 服务器应用中,BIO 可以快速搭建起基本的通信框架。

然而,当面对大量连接请求时,BIO 的局限性就会凸显出来。首先,为了处理每个连接,服务器需要创建大量的监听线程,而线程的创建和销毁会消耗大量的系统资源,包括内存、CPU 时间等。其次,由于 I/O 操作是阻塞的,当一个线程在进行 I/O 操作时,它会被阻塞,无法执行其他任务,这就导致了线程资源的浪费。例如,当一个客户端连接到服务器后,如果长时间没有数据发送,那么处理该客户端的线程就会一直阻塞在读取数据的操作上,无法为其他客户端提供服务。此外,大量线程的存在还会导致线程上下文切换频繁,进一步降低系统的性能。

二、NIO 的应运而生

2.1 时代需求催生 NIO

随着互联网的飞速发展,软件系统面临的并发访问压力与日俱增。在传统的 BIO 模型下,服务器为每个客户端连接创建一个独立的线程进行处理。当并发连接数达到一定规模时,大量线程的创建、管理和销毁会消耗大量的系统资源,包括内存、CPU 时间等 。同时,由于 I/O 操作的阻塞特性,当线程在进行 I/O 操作时,会被阻塞,无法执行其他任务,导致线程资源的浪费,系统的整体性能和并发处理能力受到严重制约。

为了应对这些挑战,Java 在 1.4 版本引入了 NIO(New I/O 或 Non - Blocking I/O)。NIO 的出现旨在提供一种更高效、更灵活的 I/O 处理方式,以满足高并发场景下的性能需求。NIO 基于通道(Channel)和缓冲区(Buffer)进行操作,采用非阻塞 I/O 和多路复用技术,允许一个线程管理多个 I/O 通道,大大减少了线程的数量和上下文切换开销,提高了系统的并发处理能力和 I/O 操作效率。

2.2 NIO 的核心组件

NIO 包含了三个核心组件:Selector(多路复用器)、Channel(通道)和 Buffer(缓冲区)。这三个组件相互协作,共同实现了 NIO 的高效非阻塞 I/O 操作。

2.2.1 Selector(多路复用器)

Selector 是 NIO 的核心组件之一,它允许一个线程同时监听多个 Channel 的事件,如连接建立、数据可读、数据可写等。通过 Selector,线程可以在多个 Channel 之间进行高效的切换,避免了线程的阻塞和资源的浪费 。

在使用 Selector 时,首先需要将 Channel 注册到 Selector 上,并指定需要监听的事件类型。Selector 会不断地轮询注册在其上的 Channel,当某个 Channel 上有感兴趣的事件发生时,Selector 会返回对应的 SelectionKey 集合,通过这些 SelectionKey 可以获取到发生事件的 Channel,并进行相应的处理。

例如,在一个服务器应用中,可以使用 Selector 来监听多个客户端的连接请求和数据传输。当有新的客户端连接请求到达时,Selector 会通知服务器线程,服务器线程可以创建新的 Channel 来处理该连接;当有客户端发送数据时,Selector 也会通知服务器线程,服务器线程可以从对应的 Channel 中读取数据并进行处理。这样,一个服务器线程就可以同时处理多个客户端的请求,大大提高了服务器的并发处理能力。

2.2.2 Channel(通道)

Channel 是对操作系统底层 I/O 通道的抽象,它提供了一种与 I/O 设备进行交互的方式。与传统的 BIO 中的流(Stream)不同,Channel 是双向的,可以同时进行读和写操作 。

在 NIO 中,有多种类型的 Channel,如 FileChannel 用于文件 I/O 操作,SocketChannel 和 ServerSocketChannel 用于 TCP 网络通信,DatagramChannel 用于 UDP 网络通信等。每个 Channel 都可以注册到 Selector 上,以便 Selector 能够监听其事件。

例如,使用 SocketChannel 进行网络通信时,可以通过调用SocketChannel.open()方法创建一个 SocketChannel 实例,然后通过configureBlocking(false)方法将其设置为非阻塞模式,再将其注册到 Selector 上,监听连接建立和数据可读事件。当有数据可读时,就可以从 SocketChannel 中读取数据。

2.2.3 Buffer(缓冲区)

Buffer 是一个用于存储数据的缓冲区,它在 NIO 中扮演着数据读写的中间角色。所有的数据操作都需要通过 Buffer 来进行,Channel 从数据源读取数据到 Buffer 中,然后程序从 Buffer 中读取数据进行处理;程序将处理后的数据写入 Buffer,再由 Channel 将 Buffer 中的数据写入到目标数据源 。

Java NIO 提供了多种类型的 Buffer,如 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分别用于存储不同类型的数据。其中,ByteBuffer 是最常用的 Buffer 类型,它可以用于存储字节数据。

ByteBuffer 又分为 HeapByteBuffer 和 DirectByteBuffer。HeapByteBuffer 是基于 Java 堆内存的缓冲区,它的创建和销毁由 Java 虚拟机的垃圾回收机制管理,优点是使用方便,与 Java 对象交互简单;缺点是在进行 I/O 操作时,可能需要在堆内存和直接内存之间进行数据拷贝,影响性能。DirectByteBuffer 是基于直接内存(堆外内存)的缓冲区,它直接在操作系统的物理内存中分配空间,不需要经过 Java 堆,因此在进行 I/O 操作时可以减少数据拷贝,提高性能,但它的创建和销毁需要手动管理,使用不当可能会导致内存泄漏。

例如,在使用 FileChannel 读取文件时,可以创建一个 ByteBuffer,然后调用 FileChannel 的read(ByteBuffer buffer)方法将文件中的数据读取到 ByteBuffer 中,再从 ByteBuffer 中读取数据进行处理。在写入数据时,先将数据写入 ByteBuffer,然后调用 FileChannel 的write(ByteBuffer buffer)方法将 ByteBuffer 中的数据写入文件。

三、BIO 与 NIO 的全面对比

3.1 功能点差异

BIO 和 NIO 在功能实现上存在显著差异,这些差异决定了它们在不同场景下的适用性。

从数据处理方式来看,BIO 基于流(Stream)进行操作,数据是顺序、连续地从流中读取或写入 ,就像水流一样,数据的读取和写入是线性的。而 NIO 引入了缓冲区(Buffer)的概念,数据先被读取到缓冲区中,然后再从缓冲区进行处理 。缓冲区提供了更灵活的数据处理方式,例如可以对缓冲区中的数据进行随机访问、标记和重置等操作。

在阻塞特性方面,BIO 是阻塞式的 I/O 模型。当一个线程执行 I/O 操作时,如从输入流中读取数据或向输出流中写入数据,线程会被阻塞,直到操作完成 。这意味着在 I/O 操作执行期间,线程无法执行其他任务,只能等待。例如,在一个简单的网络通信程序中,当服务器线程调用InputStream的read()方法读取客户端发送的数据时,如果没有数据可读,线程就会一直阻塞在这个read()操作上。而 NIO 是非阻塞式的,当线程执行 I/O 操作时,如果数据还没有准备好,线程不会被阻塞,而是立即返回 。线程可以继续执行其他任务,然后在适当的时候再次检查 I/O 操作的状态。例如,在 NIO 的网络编程中,SocketChannel在设置为非阻塞模式后,调用read()方法时,如果没有数据可读,会立即返回一个状态值,告知调用者当前没有数据可读,而不会阻塞线程。

从操作基础来看,BIO 主要基于字节流(InputStream和OutputStream)和字符流(Reader和Writer)进行操作,字节流用于处理二进制数据,字符流用于处理文本数据 。而 NIO 则基于通道(Channel)和缓冲区(Buffer)进行操作。通道是对 I/O 设备的抽象,它提供了一种与 I/O 设备进行交互的方式,并且是双向的,可以同时进行读和写操作 。缓冲区则是用于存储数据的内存块,所有的数据操作都需要通过缓冲区来进行。

在数据传输方向上,BIO 中的流通常是单向的,要么是输入流(InputStream或Reader)用于读取数据,要么是输出流(OutputStream或Writer)用于写入数据 。而 NIO 中的通道是双向的,可以同时进行读和写操作,这使得 NIO 在数据传输上更加灵活 。例如,使用SocketChannel进行网络通信时,可以通过同一个通道既读取来自客户端的数据,又向客户端发送数据。

3.2 底层原理剖析

3.2.1 BIO 的底层原理

BIO 的底层原理基于操作系统的阻塞 I/O 机制。在 BIO 中,当一个客户端与服务器建立连接时,服务器会为每个客户端连接创建一个独立的线程 。这个线程负责处理该客户端的所有 I/O 操作,包括读取客户端发送的数据和向客户端发送响应数据。

当服务器线程调用ServerSocket的accept()方法监听客户端连接时,该方法会阻塞线程,直到有新的客户端连接请求到达 。一旦有新的连接请求,accept()方法会返回一个新的Socket对象,代表与客户端的连接。然后,服务器会为这个Socket创建一个新的线程,用于处理该客户端的 I/O 操作。

在处理客户端的 I/O 操作时,例如读取客户端发送的数据,服务器线程会调用Socket的InputStream的read()方法 。这个方法会阻塞线程,直到有数据可读。如果没有数据可读,线程会一直等待,直到有数据到达或者连接关闭。当有数据可读时,read()方法会将数据读取到缓冲区中,然后返回读取的字节数。同样,在向客户端发送数据时,调用Socket的OutputStream的write()方法也会阻塞线程,直到数据被完全写入。

这种一个客户端对应一个线程的处理方式,虽然简单直观,但在高并发场景下存在严重的性能问题。因为每个线程都需要占用一定的系统资源,包括栈空间、CPU 时间等。当并发连接数较多时,大量线程的创建和管理会消耗大量的系统资源,导致系统性能下降。此外,由于 I/O 操作的阻塞特性,当线程在进行 I/O 操作时,会被阻塞,无法执行其他任务,这就导致了线程资源的浪费。

3.2.2 NIO 的底层原理

NIO 的底层原理基于操作系统的多路复用(Multiplexing)和非阻塞 I/O 机制。在 NIO 中,核心组件包括 Selector(多路复用器)、Channel(通道)和 Buffer(缓冲区) 。

Selector 是 NIO 的关键组件之一,它允许一个线程同时监听多个 Channel 的事件 。Selector 通过内部的轮询机制,不断地检查注册在其上的 Channel 是否有感兴趣的事件发生,如连接建立、数据可读、数据可写等。当某个 Channel 上有事件发生时,Selector 会将该 Channel 对应的 SelectionKey 加入到已选择键集合中,程序可以通过遍历这个集合来获取发生事件的 Channel,并进行相应的处理。

Channel 是对操作系统底层 I/O 通道的抽象,它提供了一种与 I/O 设备进行交互的方式 。Channel 可以注册到 Selector 上,以便 Selector 能够监听其事件。与 BIO 中的流不同,Channel 是双向的,可以同时进行读和写操作。例如,SocketChannel用于 TCP 网络通信,它可以通过configureBlocking(false)方法设置为非阻塞模式,在这种模式下,调用read()和write()方法时,如果数据没有准备好,不会阻塞线程,而是立即返回。

Buffer 是一个用于存储数据的缓冲区,它在 NIO 中扮演着数据读写的中间角色 。所有的数据操作都需要通过 Buffer 来进行,Channel 从数据源读取数据到 Buffer 中,然后程序从 Buffer 中读取数据进行处理;程序将处理后的数据写入 Buffer,再由 Channel 将 Buffer 中的数据写入到目标数据源。例如,在使用SocketChannel读取数据时,首先创建一个ByteBuffer,然后调用SocketChannel的read(ByteBuffer buffer)方法将数据读取到ByteBuffer中,再从ByteBuffer中读取数据进行处理。

NIO 的多路复用机制通过 Selector 实现,它使得一个线程可以同时管理多个 Channel,大大减少了线程的数量和上下文切换开销,提高了系统的并发处理能力和 I/O 操作效率。在一个聊天服务器中,可以使用一个 Selector 来监听多个客户端的SocketChannel,当有新的客户端连接请求到达时,Selector 会通知服务器线程,服务器线程可以创建新的SocketChannel来处理该连接;当有客户端发送数据时,Selector 也会通知服务器线程,服务器线程可以从对应的SocketChannel中读取数据并进行处理。这样,一个服务器线程就可以同时处理多个客户端的请求,提高了服务器的并发处理能力。

3.3 适用业务场景

BIO 适用于连接数目少且固定的场景,因为它为每个连接创建一个线程,在连接数较少时,线程资源的消耗相对较小,且编程简单,易于理解和维护 。例如,一些小型的企业内部应用,可能只需要与少量的客户端进行通信,这种情况下使用 BIO 可以快速搭建起通信框架,并且由于连接数固定,不会出现线程资源耗尽的问题。另外,对于一些对服务器资源要求不高,但对程序的简单性和可读性要求较高的场景,BIO 也是一个不错的选择。比如,一些简单的命令行工具,需要与用户进行交互,读取用户输入并返回结果,使用 BIO 可以方便地实现这种简单的 I/O 操作。

NIO 适用于连接数目多且连接短(轻操作)的高并发场景 。由于 NIO 采用非阻塞 I/O 和多路复用技术,一个线程可以管理多个连接,大大减少了线程的数量和上下文切换开销,能够高效地处理大量并发连接 。例如,在聊天服务器中,会有大量的客户端同时连接到服务器,并且每个客户端的消息发送和接收操作通常都是轻量级的,使用 NIO 可以有效地处理这些并发连接,提高服务器的性能和响应速度。同样,弹幕系统也是一个典型的高并发场景,大量的用户同时发送弹幕消息,NIO 可以快速地处理这些消息,保证弹幕的实时性。此外,在一些服务器间的通信场景中,也经常会涉及到大量的连接,NIO 的高性能和高并发处理能力使其成为理想的选择。

四、Java 实现 BIO 与 NIO 的样例展示

4.1 BIO 示例代码

4.1.1 简单 BIO 实现

下面是一个简单的 BIO(Blocking I/O)实现示例,包括服务端和客户端代码。该示例展示了如何使用 Java 的ServerSocket和Socket进行基本的网络通信,并进行数据的读写操作。

BIO 服务端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8080)) {System.out.println("BIO Server is listening on port 8080");while (true) {// 监听客户端连接,accept()方法会阻塞,直到有新的客户端连接Socket socket = serverSocket.accept();System.out.println("New client connected: " + socket.getInetAddress());// 为每个客户端连接创建一个新的线程来处理通信new Thread(() -> {try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {String inputLine;while ((inputLine = in.readLine())!= null) {System.out.println("Received from client: " + inputLine);// 向客户端发送响应out.println("Server response: " + inputLine);if ("exit".equalsIgnoreCase(inputLine)) {break;}}} catch (IOException e) {e.printStackTrace();} finally {try {socket.close();} catch (IOException e) {e.printStackTrace();}}}).start();}} catch (IOException e) {e.printStackTrace();}}
}

BIO 客户端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class BIOClient {public static void main(String[] args) {try (Socket socket = new Socket("localhost", 8080);BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {String userInput;while ((userInput = stdIn.readLine())!= null) {out.println(userInput);System.out.println("Sent to server: " + userInput);String response = in.readLine();System.out.println("Received from server: " + response);if ("exit".equalsIgnoreCase(userInput)) {break;}}} catch (UnknownHostException e) {System.out.println("Don't know about host: localhost");e.printStackTrace();} catch (IOException e) {System.out.println("Couldn't get I/O for the connection to: localhost");e.printStackTrace();}}
}

在上述代码中,BIO 服务端通过ServerSocket监听 8080 端口,当有客户端连接时,创建一个新的线程来处理与该客户端的通信。在处理线程中,使用BufferedReader从客户端读取数据,使用PrintWriter向客户端发送数据。BIO 客户端通过Socket连接到服务端,同样使用BufferedReader和PrintWriter进行数据的读写操作。

4.1.2 线程池优化的 BIO

为了减少线程创建和销毁的开销,提高性能,可以使用线程池来优化 BIO。下面是一个使用线程池优化后的 BIO 服务端代码示例:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolOptimizedBIOServer {private static final int THREAD_POOL_SIZE = 10;public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);try (ServerSocket serverSocket = new ServerSocket(8080)) {System.out.println("ThreadPoolOptimizedBIO Server is listening on port 8080");while (true) {// 监听客户端连接,accept()方法会阻塞,直到有新的客户端连接Socket socket = serverSocket.accept();System.out.println("New client connected: " + socket.getInetAddress());// 将客户端连接的处理任务提交到线程池executorService.submit(() -> {try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {String inputLine;while ((inputLine = in.readLine())!= null) {System.out.println("Received from client: " + inputLine);// 向客户端发送响应out.println("Server response: " + inputLine);if ("exit".equalsIgnoreCase(inputLine)) {break;}}} catch (IOException e) {e.printStackTrace();} finally {try {socket.close();} catch (IOException e) {e.printStackTrace();}}});}} catch (IOException e) {e.printStackTrace();} finally {executorService.shutdown();}}
}

在这个示例中,使用Executors.newFixedThreadPool(THREAD_POOL_SIZE)创建了一个固定大小为 10 的线程池。当有新的客户端连接时,不再为每个连接创建一个新的线程,而是将处理任务提交到线程池中,由线程池中的线程来处理。这样可以减少线程的创建和销毁开销,提高系统的性能和资源利用率 。同时,在程序结束时,调用executorService.shutdown()方法来关闭线程池,释放资源。

4.2 NIO 示例代码

4.2.1 NIO 基础示例

下面是一个 NIO(New I/O)的基础示例,展示了 NIO 中Selector、Channel和Buffer的协同工作。该示例包括 NIO 服务端和客户端代码,演示了如何使用 NIO 进行网络通信。

NIO 服务端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {public static void main(String[] args) {try (Selector selector = Selector.open();ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(8080));serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("NIO Server is listening on port 8080");while (true) {// 阻塞,直到有感兴趣的事件发生selector.select();Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {// 处理新的客户端连接ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel clientChannel = server.accept();clientChannel.configureBlocking(false);// 注册读事件clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("Accepted new connection from client: " + clientChannel.getRemoteAddress());} else if (key.isReadable()) {// 处理客户端发送的数据SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String message = new String(data);System.out.println("Received from client: " + message);// 向客户端发送响应ByteBuffer responseBuffer = ByteBuffer.wrap(("Server response: " + message).getBytes());clientChannel.write(responseBuffer);} else if (bytesRead == -1) {// 客户端关闭连接clientChannel.close();}}}}} catch (IOException e) {e.printStackTrace();}}
}

NIO 客户端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {public static void main(String[] args) {try (SocketChannel socketChannel = SocketChannel.open()) {socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("localhost", 8080));while (!socketChannel.finishConnect()) {// 可以在此处执行其他任务}System.out.println("Connected to server");ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("Hello, Server".getBytes());buffer.flip();socketChannel.write(buffer);buffer.clear();int bytesRead = socketChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String response = new String(data);System.out.println("Received from server: " + response);}} catch (IOException e) {e.printStackTrace();}}
}

在上述 NIO 服务端代码中,首先创建了一个Selector和一个非阻塞的ServerSocketChannel,并将ServerSocketChannel注册到Selector上,监听OP_ACCEPT事件。当有新的客户端连接时,接受连接并将新的SocketChannel注册到Selector上,监听OP_READ事件。当有可读事件发生时,从SocketChannel中读取数据,并向客户端发送响应。NIO 客户端代码中,创建一个非阻塞的SocketChannel,连接到服务端,发送数据并接收服务端的响应。

4.2.2 复杂 NIO 场景示例

下面是一个更复杂的 NIO 应用场景示例,实现了一个简单的文件服务器。该示例展示了 NIO 在实际应用中的强大功能,包括文件的读取和传输。

NIO 文件服务器服务端代码

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOFileServer {private static final int PORT = 8081;private static final String FILE_DIRECTORY = "files";public static void main(String[] args) {try (Selector selector = Selector.open();ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(PORT));serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("NIO File Server is listening on port " + PORT);while (true) {selector.select();Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();keyIterator.remove();if (key.isAcceptable()) {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel clientChannel = server.accept();clientChannel.configureBlocking(false);clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("Accepted new connection from client: " + clientChannel.getRemoteAddress());} else if (key.isReadable()) {SocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = clientChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);String fileName = new String(data).trim();System.out.println("Received file request for: " + fileName);File file = new File(FILE_DIRECTORY + File.separator + fileName);if (file.exists() && file.isFile()) {try (FileInputStream fileInputStream = new FileInputStream(file)) {buffer = ByteBuffer.allocate(1024);while ((bytesRead = fileInputStream.read(buffer.array()))!= -1) {buffer.flip();clientChannel.write(buffer);buffer.clear();}}} else {System.out.println("File not found: " + fileName);clientChannel.write(ByteBuffer.wrap("File not found".getBytes()));}} else if (bytesRead == -1) {clientChannel.close();}}}}} catch (IOException e) {e.printStackTrace();}}
}

NIO 文件服务器客户端代码

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOFileClient {private static final String SERVER_ADDRESS = "localhost";private static final int SERVER_PORT = 8081;private static final String DOWNLOAD_DIRECTORY = "downloads";public static void main(String[] args) {if (args.length!= 1) {System.out.println("Usage: java NIOFileClient <fileName>");return;}String fileName = args[0];try (SocketChannel socketChannel = SocketChannel.open()) {socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress(SERVER_ADDRESS, SERVER_PORT));while (!socketChannel.finishConnect()) {// 可以在此处执行其他任务}System.out.println("Connected to file server");ByteBuffer buffer = ByteBuffer.wrap(fileName.getBytes());socketChannel.write(buffer);buffer = ByteBuffer.allocate(1024);FileOutputStream fileOutputStream = new FileOutputStream(DOWNLOAD_DIRECTORY + "/" + fileName);int bytesRead;while ((bytesRead = socketChannel.read(buffer))!= -1) {buffer.flip();byte[] data = new byte[buffer.remaining()];buffer.get(data);if (new String(data).equals("File not found")) {System.out.println("File not found on server");break;}fileOutputStream.write(data);buffer.clear();}fileOutputStream.close();} catch (IOException e) {e.printStackTrace();}}
}

在这个 NIO 文件服务器示例中,服务端监听指定端口,当有客户端连接并发送文件名请求时,服务端检查文件是否存在,如果存在则将文件内容读取并发送给客户端;如果文件不存在,则向客户端发送 “File not found”。客户端连接到服务端,发送文件名请求,接收文件内容并保存到本地指定目录。如果接收到 “File not found”,则提示用户文件在服务器上不存在。

五、总结与展望

从 BIO 到 NIO 的演变,是 Java I/O 领域顺应时代发展的重大变革。BIO 作为 Java 早期的 I/O 模型,以其简单直观的编程方式,在早期的应用开发中发挥了重要作用 ,但在高并发场景下,其阻塞式的 I/O 操作和线程资源的高消耗,限制了系统的性能和扩展性。

NIO 的出现,为解决 BIO 的困境提供了全新的思路和方法。通过引入 Selector、Channel 和 Buffer 等核心组件,NIO 构建了基于事件驱动的非阻塞 I/O 模型,实现了一个线程管理多个 I/O 通道,大大提高了系统的并发处理能力和 I/O 操作效率 。在高并发场景下,NIO 的优势尤为显著,如聊天服务器、弹幕系统、文件服务器等应用场景中,NIO 能够高效地处理大量并发连接,提升系统的性能和响应速度 。

展望 Java I/O 的未来发展趋势,随着硬件技术的不断进步和应用场景的日益复杂,对 I/O 性能的要求也将越来越高。一方面,NIO 有望在现有基础上进一步优化和完善,提升其在不同场景下的性能表现和稳定性 。例如,在缓冲区管理、通道操作等方面进行更高效的实现,减少资源消耗和性能开销。另一方面,随着异步编程、分布式系统等技术的发展,Java I/O 可能会与这些技术更紧密地结合,以满足分布式、高并发环境下的复杂 I/O 需求 。如在分布式文件系统中,NIO 可以为数据的高效传输和处理提供支持;在异步通信框架中,NIO 的非阻塞特性能够更好地实现异步操作,提高系统的响应能力。此外,随着人工智能、大数据等新兴领域的快速发展,Java I/O 也需要不断演进,以适应这些领域对大规模数据处理和高速数据传输的需求 。

对于 Java 开发者来说,深入理解 BIO 和 NIO 的原理、特性及适用场景,能够根据不同的业务需求选择合适的 I/O 模型,是提升 Java 应用性能和开发效率的关键 。同时,关注 Java I/O 的发展动态,不断学习和掌握新的 I/O 技术和应用方法,将有助于在未来的 Java 开发中,更好地应对各种挑战,开发出更高效、更可靠的应用程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/12880.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于springboot的体质测试数据分析及可视化设计

作者&#xff1a;学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等 文末获取“源码数据库万字文档PPT”&#xff0c;支持远程部署调试、运行安装。 项目包含&#xff1a; 完整源码数据库功能演示视频万字文档PPT 项目编码&#xff1…

离散时间傅里叶变换(DTFT)公式详解:周期性与连续性剖析

摘要 离散时间傅里叶变换&#xff08;DTFT&#xff09;是数字信号处理领域的重要工具&#xff0c;它能将离散时间信号从时域转换到频域&#xff0c;揭示信号的频率特性。本文将深入解读DTFT公式&#xff0c;详细阐述其具有周期性和连续性的原因&#xff0c;帮助读者全面理解DT…

Docker 安装详细教程(适用于CentOS 7 系统)

目录 步骤如下&#xff1a; 1. 卸载旧版 Docker 2. 配置 Docker 的 YUM 仓库 3. 安装 Docker 4. 启动 Docker 并验证安装 5. 配置 Docker 镜像加速 总结 前言 Docker 分为 CE 和 EE 两大版本。CE即社区版&#xff08;免费&#xff0c;支持周期7个月&#xff09;&#xf…

Mac mini m4本地跑大模型(ollama + llama + ComfyUI + Stable Diffusion | flux)

change log 2024-12-11 10:28&#xff08;推荐重新观看&#xff09; 针对绘画大模型的使用做进一步的详细操作&#xff08;flux1dev&#xff09; 见篇节&#xff08;绘画大模型&#xff09; 2024-12-10 更新了基础的chat大模型和绘画大模型的基础环境搭建。 安装chat大模型&am…

基于PLC的电热水器的水箱水位控制系统(论文+源码

1总体方案设计 本设计基于PLC的电热水器的水箱水位控制系统的整体结构如图2.1所示&#xff0c;系统采用S7-1200 PLC为控制器&#xff0c;可以实现电热水器水箱中的水位、水温检测&#xff0c;并且用户可以设定目标水位和水温&#xff0c;在自动模式下&#xff0c;当水位低于低…

专业学习|一文了解并实操自适应大邻域搜索(讲解代码)

一、自适应大邻域搜索概念介绍 自适应大邻域搜索&#xff08;Adaptive Large Neighborhood Search&#xff0c;ALNS&#xff09;是一种用于解决组合优化问题的元启发式算法。以下是关于它的详细介绍&#xff1a; -自适应大领域搜索的核心思想是&#xff1a;破坏解、修复解、动…

【Leetcode】4. 寻找两个正序数组的中位数

一、题目描述 给定两个大小分别为 m m m 和 n n n 的正序&#xff08;从小到大&#xff09;数组 n u m s 1 nums1 nums1 和 n u m s 2 nums2 nums2。请你找出并返回这两个正序数组的中位数 。 算法的时间复杂度应该为 O ( l o g ( m n ) ) O(log (mn)) O(log(mn)) 。 示…

如何优化垃圾回收机制?

垃圾回收机制 掌握 GC 算法之前&#xff0c;我们需要先弄清楚 3 个问题。第一&#xff0c;回收发生在哪里&#xff1f;第二&#xff0c;对象在 什么时候可以被回收&#xff1f;第三&#xff0c;如何回收这些对象&#xff1f; 回收发生在哪里&#xff1f; JVM 的内存区域中&…

吴签磁力_简单多功能的磁力搜索工具

磁力搜索&#xff0c;一个比较冷门的话题&#xff0c;但是它能解决我们对于音乐、影视、游戏、软件等资源的需求&#xff0c;今天给大家安利一款深夜学习必备的磁力搜索引擎——吴签磁力。 “吴签磁力”是一款集搜索、下载于一体的多功能磁力搜索引擎&#xff0c;它巧妙地将百度…

spring基础总结

先修知识&#xff1a;依赖注入&#xff0c;反转控制&#xff0c;生命周期 IDEA快捷键 Ctrl Altm:提取方法&#xff0c;设置trycatch 通用快捷键&#xff1a; Ctrl F&#xff1a;在当前文件中查找文本。Ctrl R&#xff1a;在当前文件中替换文本。Ctrl Z&#xff1a;撤销…

MyBatis XML文件配置

目录 一、 配置连接字符串和MyBatis 二、书写持久层代码 2.1 添加Mapper接口 2.2 添加UserlnfoXMLMapper.xml 三、增删改查 3.1 、增&#xff08;Insert&#xff09; 3.2、删&#xff08;Delete) 3.3、改 (Update) 3.4、查 (Select) MyBatisXML的方式需要以下两步&am…

QT:对象树

1.概念 Qt 中的对象树是一种以树形结构组织 Qt 对象的方式。当创建一个QObject&#xff08;Qt 中大多数类的基类&#xff09;或其派生类的对象时&#xff0c;可以为其指定一个父对象&#xff08;parent&#xff09;。这个对象就会被添加到其父对象的子对象列表中&#xff0c;形…

设备通过国标GB28181接入EasyCVR,显示在线但视频无法播放的原因排查

安防监控EasyCVR平台支持多种视频源接入&#xff0c;包括但不限于IP摄像头、NVR、编码器、流媒体服务器等。平台采用高效的视频流接入技术&#xff0c;支持大规模视频流的并发接入&#xff0c;确保视频流的稳定性和流畅性。 有用户反馈&#xff0c;项目现场使用国标GB28181接入…

本地安装部署deepseek

在截图下载工具(地址不方便粘贴过不审核)复制安装程序链接下载模型管理工具ollama&#xff0c;下一步下一步&#xff0c;有需要也可以下载linux版的 githup&#xff1a;https://github.com/ollama/ollama/releases/tag/v0.5.7 安装程序&#xff1a;https://github.com/ollama…

熟练掌握Http协议

目录 基本概念请求数据Get请求方式和Post请求方式 响应数据响应状态码 基本概念 Http协议全称超文本传输协议(HyperText Transfer Protocol)&#xff0c;是网络通信中应用层的协议&#xff0c;规定了浏览器和web服务器数据传输的格式和规则 Http应用层协议具有以下特点&#…

在Mapbox GL JS中“line-pattern”的使用详解

在Mapbox GL JS中&#xff0c;line-pattern 是一种用于在地图上绘制带有图案的线条的样式属性。通过 line-pattern&#xff0c;你可以使用自定义的图像作为线条的图案&#xff0c;而不是使用纯色或渐变。 1. 基本概念 line-pattern: 该属性允许你指定一个图像作为线条的图案。…

QT:信号和槽

目录 1.概念 2.信号和槽的使用 2.1代码的方式使用 2.1.1.使用connect关联 2.2图形化界面的方式使用 2.2.1使用流程 2.2.2使用名字关联槽函数 3.自定义信号和槽函数 3.1自定义槽函数 3.2自定义信号 4.总结 1.概念 信号和槽是QT特有的一种机制&#xff0c;信号和槽都是…

【数据分析】豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)

豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask) 豆瓣电影Top250官网:https://movie.douban.com/top250写在前面 实验目的:实现豆瓣电影Top250详情的数据分析与Web网页可视化。电脑系统:Windows使用软件:PyCharm、NavicatPython版本:Python 3.…

simpleQtLogger日志库的使用

simpleQtLogger日志库的使用 simpleQtLogger日志库的使用1. 使用2. 修改点控制台中文乱码修复qt6没有setCodec()修复void*强转数值修复打印日志级别修改 simpleQtLogger日志库的使用 B站上看到一个简单的qt日志库&#xff0c;这里记录一下它的使用。 但是我并不知道源码是谁写…

Linux中安装rabbitMQ

使用docker安装 Linux中还没有安装docker的可以看我之前的视频&#xff0c;先把docker安装了。 Docker的安装_docker version 25.0.1-CSDN博客 检查是否有docker docker -v 上传mq的tar包 我们把mq的tar包上传到我们的Linux服务器中&#xff0c;随后加载成docker的镜像。 加载…