JAVA基础第一弹-IO模型

67 阅读9分钟

大家好,今天来和大家分享一下IO模型~

Java 中的 I/O 模型主要包括三种:BIO (Blocking I/O),NIO (Non-blocking I/O) 和 AIO (Asynchronous I/O)。

1. BIO (Blocking I/O)

基本概念

在Java中,BIO(Blocking I/O)是传统的I/O模型,它基于流(Stream)的概念来实现数据的读写操作。在这个模型中,当一个线程发起I/O请求时,该线程会被阻塞直到I/O操作完成,这意味着在等待期间,线程无法执行其他任务。这种模型简单直观,易于理解和使用,但在高并发场景下可能会遇到性能瓶颈。

BIO的主要特点

  • 阻塞性:当一个线程调用read或write等方法时,如果没有数据可读或者数据缓冲区未准备好,那么该线程会一直等待,直到有数据可以读取或缓冲区准备好为止。这导致了在处理大量并发连接时,需要为每个连接分配一个独立的线程,以确保每个连接都能得到及时响应。
  • 一对一模式:通常情况下,BIO采用的是“一连接一线程”的模型,即每当有一个新的客户端连接请求到来时,服务器端就会创建一个新的线程与之对应。这种模式虽然能够很好地支持单个连接的数据交互,但是随着连接数的增加,线程数量也会相应增加,从而可能导致系统资源耗尽,比如线程上下文切换带来的开销增大、内存占用过多等问题。

应用场景

尽管BIO存在上述不足,但在某些特定的应用场景下,它仍然是一个合适的选择:

  • 当并发连接数不多且对延迟要求不高时,BIO模型可以提供较为简单的解决方案。
  • 对于一些简单的应用或服务,如文件上传下载、小型网站等,BIO模型往往已经足够满足需求。

 

使用示例

下面是 BIO 服务端示例,用于监听并响应客户端的连接请求:

import java.net.*;

public class BioServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO 服务端已启动,等待客户端连接...");

        while (true) {
            Socket socket = serverSocket.accept(); // 阻塞等待连接
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                        String inputLine;
                        while ((inputLine = in.readLine()) != null) {
                            if ("bye".equals(inputLine)) break; // 如果客户端发送 "bye",则断开连接
                            System.out.println("收到客户端消息: " + inputLine);
                            out.println("Echo: " + inputLine); // 回应客户端
                        }
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

2. NIO (Non-blocking I/O)

基本概念

NIO(Non-blocking I/O),也称为New I/O,是在Java 1.4版本中引入的一种新的I/O操作方式。与传统的BIO不同,NIO提供了非阻塞式的I/O操作,使得一个线程可以管理多个输入输出通道,极大地提高了程序在高并发场景下的性能和效率。NIO的核心组件包括Buffer(缓冲区)、Channel(通道)和Selector(选择器)。

NIO的主要特点

  • 非阻塞性:在NIO中,当线程尝试从Channel读取或写入数据时,如果当前没有数据可读或没有空间可写,那么线程不会被阻塞,而是立即返回。这允许线程继续执行其他任务,直到数据可用或有空间可写。
  • 多路复用:通过Selector,一个线程可以同时监视多个Channel的状态变化(如是否可读、是否可写)。当某个Channel准备就绪时,线程可以进行相应的I/O操作。这种方式使得一个线程能够高效地管理和响应多个连接,而不需要为每个连接创建独立的线程。
  • 直接内存:NIO中的Buffer可以直接映射到操作系统内核中的缓冲区,这允许数据在传输过程中直接在操作系统层面操作,减少了用户态和内核态之间的数据复制次数,从而提高了数据传输的效率。
  • 零拷贝:NIO还支持零拷贝技术,即在文件传输过程中,数据可以直接从磁盘读取到网络接口卡的缓冲区,而无需经过JVM的内存,进一步减少了CPU和内存的使用。

应用场景

NIO特别适合于高并发的网络应用,例如Web服务器、数据库服务器等。由于其高效的I/O处理能力,NIO成为了现代高性能网络应用的基础之一。在这些应用中,可能需要同时处理成千上万个连接,使用NIO可以显著减少线程的数量,降低系统的资源消耗,提高系统的整体性能。

NIO与BIO的区别

  • 并发处理能力:NIO通过单个线程即可处理多个连接的I/O操作,而BIO需要为每个连接分配一个线程。
  • 资源消耗:NIO在处理大量并发连接时,对系统资源的消耗远低于BIO。
  • 编程复杂度:NIO的编程模型相对于BIO来说更为复杂,因为它涉及到更多的抽象概念,如Buffer、Channel和Selector等。

使用示例

下面是一个简单的 NIO 服务端示例,展示如何使用 Selector 处理多个连接:

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;

public class NioServer {

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(8080));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO 服务端已启动,等待客户端连接...");
        while (true) {
            selector.select();
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();

                if (key.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel client = ssc.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("新客户端连接: " + client);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int read = client.read(buffer);
                    if (read == -1) {
                        client.close();
                    } else {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        String msg = new String(data).trim();
                        System.out.println("收到客户端消息: " + msg);
                        client.write(ByteBuffer.wrap(("Echo: " + msg).getBytes()));
                    }
                }
            }
        }
    }
}

3. AIO (Asynchronous I/O)

基本概念

NIO(Non-blocking I/O)是Java中一种非常重要的I/O模型,尤其适用于高并发场景。NIO的核心组件包括Buffer、Channel、Selector和File Locking。下面我们逐一介绍这些概念及其工作原理。

核心组件

1. Buffer(缓冲区)

Buffer是NIO中的基本数据容器,所有数据在NIO中都必须通过Buffer对象来传输。Buffer本质上是一个数组,但它提供了一套丰富的操作方法,使得数据的读写更加方便。常见的Buffer类型包括:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每个Buffer都有以下关键属性:

  • capacity:缓冲区的容量,即最大容量。
  • limit:缓冲区的界限,表示缓冲区中可以操作的数据的最大范围。
  • position:当前的位置,表示下一个要读取或写入的数据的索引。
  • mark:标记位置,用于记录一个特定的位置,可以通过reset()方法恢复到这个位置。

2. Channel(通道)

Channel是NIO中用于进行I/O操作的组件,类似于传统I/O中的流,但功能更强大。Channel可以双向读写数据,而传统的流通常是单向的。常见的Channel类型包括:

  • FileChannel
  • SocketChannel
  • ServerSocketChannel
  • DatagramChannel

Channel的主要方法包括:

  • read(Buffer):从Channel读取数据到Buffer中。
  • write(Buffer):将Buffer中的数据写入Channel。
  • close():关闭Channel。

3. Selector(选择器)

Selector是NIO中用于处理多路复用的关键组件,它可以监视多个Channel的状态变化(如是否可读、是否可写),并在某个Channel准备好时通知程序。这样,一个线程就可以管理多个Channel,而不需要为每个Channel创建一个独立的线程。

主要的方法包括:

  • open():打开一个新的Selector。
  • register(SelectableChannel, int):将Channel注册到Selector上,并指定感兴趣的事件类型(如读、写)。
  • select():阻塞等待,直到至少有一个Channel准备好。
  • selectedKeys():获取已准备好的Channel的集合。

4. File Locking(文件锁定)

文件锁定是一种机制,用于在多个进程之间协调对文件的访问。NIO提供了文件锁定的支持,可以在文件的某一部分上加锁,防止其他进程在同一时间对该部分进行修改。

工作原理

  1. 数据读取过程
    • 创建一个Buffer对象。
    • 打开一个Channel,例如FileChannel或SocketChannel。
    • 使用Channel的read()方法将数据读取到Buffer中。
    • 调整Buffer的position和limit,以便后续操作。
    • 处理Buffer中的数据。
  2. 数据写入过程
    • 创建一个Buffer对象,并填充数据。
    • 打开一个Channel,例如FileChannel或SocketChannel。
    • 使用Channel的write()方法将Buffer中的数据写入Channel。
    • 调整Buffer的position和limit,以便后续操作。
  3. 多路复用
    • 创建一个Selector对象。
    • 将多个Channel注册到Selector上,并指定感兴趣的事件类型。
    • 调用Selector的select()方法,阻塞等待,直到至少有一个Channel准备好。
    • 获取已准备好的Channel的集合,对每个Channel进行相应的I/O操作。

优势

  • 非阻塞性:NIO的非阻塞性使得一个线程可以同时处理多个I/O操作,提高了系统的并发处理能力。
  • 多路复用:通过Selector,一个线程可以高效地管理多个连接,减少了线程的数量和上下文切换的开销。
  • 直接内存:NIO支持直接内存操作,减少了数据在用户态和内核态之间的复制次数,提高了数据传输的效率。
  • 零拷贝:NIO支持零拷贝技术,进一步减少了CPU和内存的使用。

 

使用示例

 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;


public class AioServer {

    public static void main(String[] args) throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
        System.out.println("AIO 服务端已启动,等待客户端连接...");
        accept(serverSocket, latch);
        latch.await();
        serverSocket.close();
    }


    private static void accept(AsynchronousServerSocketChannel serverSocket, final CountDownLatch latch) {

        serverSocket.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {

            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                System.out.println("新客户端连接: " + client);
                accept(serverSocket, latch); // 接受下一个连接
                read(client); // 读取数据
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
                latch.countDown();
            }
        });
    }

    private static void read(final AsynchronousSocketChannel client) {
        final ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (result == -1) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    return;
                }
                attachment.flip();
                byte[] data = new byte[attachment.remaining()];
                attachment.get(data);
                String msg = new String(data).trim();
                System.out.println("收到客户端消息: " + msg);
                write(client, msg); // 回应客户端
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                exc.printStackTrace();
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private static void write(final AsynchronousSocketChannel client, String msg) {
        ByteBuffer buffer = ByteBuffer.wrap(("Echo: " + msg).getBytes());
        client.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                if (attachment.hasRemaining()) {
                    client.write(attachment, attachment, this);
                } else {
                    System.out.println("响应已发送给客户端");
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                exc.printStackTrace();
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

BIO、NIO 和 AIO 各有特点,适用于不同的应用场景。BIO 适合于连接数目少且固定的场景;NIO 适合于连接数目多且需要高效处理的场景;而 AIO 则更适合于需要异步处理 I/O 操作的场景。

欢迎大家在评论区一起讨论~