Java IO模型之BIO和NIO分析

Java IO模型之BIO和NIO分析

前言:

根据不同情况选择合适的IO模型

IO模型概述

1、BIO、NIO、AIO概述

I/O 模型简单的理解就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。Java 共支持 3 种网络编程模型 I/O 模式:BIONIOAIO

  • 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中常用的缓冲流有 BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter

  • 写入文件
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 提供了更高效和更灵活的文件操作方式,尤其适合大文件的处理。主要使用 FilesPaths 以及 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 中要么是输入流,或者是输出流,不能双向,但是 NIOBuffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的。

1. Selector(选择器)

JavaNIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器),Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。NettyIO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。

2. Channel(通道)

NIO 的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写,BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲:

常用的 Channel 类有:FileChannelDatagramChannelServerSocketChannelSocketChannelFileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannelSocketChannel 用于 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堆和内核空间之间复制数据,显著提升吞吐量。

相关推荐
笙枫16 小时前
基于AI Agent框架下的能源优化调度方案和实践 |工具函数介绍(详细)
java·人工智能·能源
我命由我1234516 小时前
Android Studio - Android Studio 去除 import 的未使用的类
android·java·ide·学习·java-ee·android studio·学习方法
沛沛老爹16 小时前
Skills高级设计模式(一):向导式工作流与模板生成
java·人工智能·设计模式·prompt·aigc·agent·web转型
程序员老徐16 小时前
Tomcat源码分析二(Tomcat启动源码分析)
java·tomcat·firefox
BD_Marathon16 小时前
SpringMVC——5种类型参数传递
android·java·数据库
a31582380616 小时前
Android 大图显示策略优化显示(二)
android·java·开发语言·javascript·kotlin·glide·图片加载
黛色正浓16 小时前
leetCode-热题100-普通数组合集(JavaScript)
java·数据结构·算法
月明长歌16 小时前
Java多线程线程池ThreadPoolExecutor理解总结:6 个核心参数 + 4 种拒绝策略(附完整示例)
java·开发语言
找不到、了16 小时前
JVM 跨代引用与 Card Table 机制
java·jvm