Java IO模型之BIO和NIO分析
前言:
根据不同情况选择合适的IO模型
IO模型概述
1、BIO、NIO、AIO概述
I/O 模型简单的理解就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。Java 共支持 3 种网络编程模型 I/O 模式:BIO、NIO、AIO。
-
BIO同步并阻塞(传统阻塞型)方式适用于连接数目比较小且固定的架构,是Java传统的I/O模型,如简单脚本工具、小文件处理(<10MB)、单线程应用。 -
NIO同步非阻塞的方式适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器、弹幕系统、服务器间通讯、大文件处理(>100MB)、高并发文件服务、实时日志处理、需要零拷贝特性。 -
AIO异步不阻塞的方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作。
2、差别举例说明
同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。
同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己.
异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家给你理发
传统BIO读写文件方式
0、BIO介绍
Java BIO(BlockingI/O) 就是传统的 Java I/O 编程,其相关的类和接口在 java.io。同步阻塞,服务器实现模式为一个连接一个线程。
1、使用字节流读写
字节流主要用于处理二进制数据,如图片、音频等,也可用于简单的文本文件读写
- 写入文件
java
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamExample {
public static void main(String[] args) {
String data = "Hello, Java File IO!";
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
fos.write(data.getBytes());
System.out.println("File written successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 读取文件
java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("input.txt")) {
int content;
while ((content = fis.read()) != -1) {
// 将读取的字节转换成字符并输出
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、使用字符流读写
字符流专门用于处理文本数据,相比字节流,字符流处理文本时更方便,能够自动处理字符编码问题。
- 写入文件
java
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
public static void main(String[] args) {
String content = "This is a sample text written using FileWriter.";
try (FileWriter fw = new FileWriter("output.txt")) {
fw.write(content);
System.out.println("Text written successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 读取文件
java
import java.io.FileReader;
import java.io.IOException;
public class FileReaderExample {
public static void main(String[] args) {
try (FileReader fr = new FileReader("input.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3、通过缓冲层提高文件操作效率
Java中常用的缓冲流有 BufferedInputStream、BufferedOutputStream、BufferedReader 和 BufferedWriter。
- 写入文件
java
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample {
public static void main(String[] args) {
String text = "BufferedWriter helps to improve performance by reducing IO operations.";
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write(text);
System.out.println("Buffered write completed.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 读取文件
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用NIO读写文件方式
0、NIO介绍
Java NIO 全称 Java non-blocking IO,被统称为 NIO(即 NewIO),是同步非阻塞的,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。
NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写
NIO 提供了更高效和更灵活的文件操作方式,尤其适合大文件的处理。主要使用 Files、Paths 以及 FileChannel 来进行文件操作。
1、使用Files类读写
Files 类提供了许多静态方法,能够一次性读取整个文件内容或写入数据到文件中。
- 写入文件
java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FilesWriteExample {
public static void main(String[] args) {
Path path = Paths.get("output.txt");
String content = "Writing to file using Files.write() method.";
try {
Files.writeString(path, content, StandardCharsets.UTF_8);
System.out.println("File written using NIO.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 读取文件
java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
public class FilesReadExample {
public static void main(String[] args) {
Path path = Paths.get("input.txt");
try {
String content = Files.readString(path);
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、 使用 FileChannel 与 ByteBuffer
FileChannel 提供了高效的文件数据传输方式,特别适合大文件的复制与传输。
- 复制文件
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelCopyExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("destination.txt");
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (inChannel.read(buffer) > 0) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
System.out.println("File copied using FileChannel.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、零拷贝修改文件
java
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数 2:0:可以直接修改的起始位置
* 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
NIO核心组件说明
0、概述
NIO 有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器),整体面向缓冲区编程,数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。大致逻辑如下:
- 一个线程从某通道发送请求或者读取数据
- 线程仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,并且线程不阻塞,该线程可以继续做其他的事情,直到数据变的可以读取之前。
- 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

Selector 对应一个线程,一个线程对应多个 Channel(连接),每个 Channel 都会对应一个 Buffer,程序切换到哪个 Channel 是由事件决定的,Event 就是一个重要的概念,Selector 会根据不同的事件,在各个通道上切换。Buffer 就是一个内存块,底层是有一个数组,数据的读取写入是通过 Buffer,这个和 BIO是不同的,BIO 中要么是输入流,或者是输出流,不能双向,但是 NIO 的 Buffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的。
1. Selector(选择器)
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器),Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2. Channel(通道)
NIO 的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写,
BIO中的Stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。 - 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel 。FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有
public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中public int write(ByteBuffer src),把缓冲区的数据写到通道中public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
3. Buffer(缓冲区)
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块 ,可以理解成是一个容器对象(含数组) ,该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
三个核心属性及操作:
- 一:创建时固定的大小,在初始化后是不可变 的。这是
ByteBuffer设计的核心原则之一,由其底层实现机制决定.
java
ByteBuffer buffer = ByteBuffer.allocate(5);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024*1024);
ByteBuffer.allocate() 是堆缓冲区,由JVM管理,受GC影响,I/O操作性能低(主要原因是需要额外的内存复制操作),受JVM的GC自动管理,无需关心内存释放和分配问题。内存大小受JVM堆内存大小限制。比较适合短期操作/小数据量
ByteBuffer.allocateDirect() 是堆外内存块(直接缓冲区)即操作系统的内存,数据直接与操作系统内核交互(称为零拷贝操作),不受JVM垃圾回收(GC)影响,I/O操作性能更高(主要原因减少内存复制),所以更适合高频I/O操作(如文件读写/网络传输)。缺点是使用的操作系统的内存,需要考虑内存大小,自己要管理内存的释放与分配问题内,不释放就会导致内存泄漏。如:在Netty等网络框架中,默认使用直接缓冲区处理网络数据包,避免在JVM堆和内核空间之间复制数据,显著提升吞吐量。