Java NIO

发布于:2024-04-29 ⋅ 阅读:(37) ⋅ 点赞:(0)

1. IO分类概述

1.1 阻塞与非阻塞

        阻塞(Blocking)和非阻塞(Nonblocking)是在计算机编程中用于描述I/O操作的两个重要概念。阻塞与非阻塞描述的是线程在访问某个资源时,在该资源没有准备就绪期间的处理方式。

        1、阻塞:阻塞是指在进行I/O操作时,当前线程会被挂起,等待数据的就绪或操作的完成。

  • 在阻塞状态下,线程会一直等待,直到条件满足或超时才会继续执行
  • 阻塞式I/O操作会导致调用线程无法执行其他任务,直到I/O操作完
  • 例如,在读取文件时,如果没有数据可用,线程会一直等待直到有数据可读

        2、非阻塞:非阻塞是指进行I/O操作时,当前线程不会被挂起,而是立即返回并继续执行其他任务。

  • 在非阻塞状态下,即使I/O操作无法立即完成,线程也可以继续执行其他操作,而不需要一直等待
  • 非阻塞式I/O操作通常会返回一个状态或错误码,指示操作是否完成或需要进一步处理
  • 应用程序可以通过不断轮询状态来判断是否可以进行下一步操作

1.2 同步与异步

        同步(synchronous)与异步(asynchronous)描述的是线程在发出请求后,是否等待结果。

        1、同步(Synchronous):同步是指程序按照顺序执行,并等待某个操作完成后再继续执行下一个操作。

  • 在同步操作中,程序主动发起请求并等待结果返回,直到结果返回后才能继续执行后续操作
  • 同步操作通常是阻塞的,即在等待结果时当前线程会被挂起,无法执行其他任务
  • 典型的同步操作包括函数调用、阻塞式I/O操作等

        2、异步(Asynchronous):异步是指程序在发起某个操作后,不需要立即等待操作完成,而是继续执行后续的操作。

  • 在异步操作中,程序不会阻塞等待结果的返回,而是通过回调、轮询或事件通知等机制来获取结果或处理完成的事件
  • 异步操作允许程序并发执行多个任务,提高了系统的并发性和响应性。典型的异步操作包括异步函数调用、非阻塞式I/O操作、异步任务等

1.3 IO的分类

        当涉及I/O操作时,可以根据是否阻塞和是否异步的角度将其分为以下四类:

        1、阻塞式I/O(Blocking I/O)

  • 特点:在进行I/O操作时,当前线程会被阻塞,直到数据就绪或操作完成
  • 工作原理:当进行I/O操作时,线程会等待直到数据准备好或操作完成,在等待期间,线程无法执行其他任务

        2、非阻塞式I/O(Non-Blocking I/O)

  • 特点:进行I/O操作时,当前线程不会被阻塞,立即返回并继续执行其他任务
  • 工作原理:当进行I/O操作时,如果数据尚未准备好或操作无法立即完成,操作会立即返回一个状态指示暂时不可用

        3、I/O多路复用(I/O Multiplexing)

  • 特点:允许一个线程同时监视多个I/O通道的就绪状态,提高了系统的并发性能
  • 工作原理:通过操作系统提供的I/O复用机制,将多个I/O通道注册到一个选择器上,然后使用选择器进行轮询以确定哪些通道就绪

        4、异步I/O(Asynchronous I/O)

  • 特点:在进行I/O操作时,不需要立即等待操作完成,可以继续执行其他任务,通过回调或事件机制来处理操作结果
  • 工作原理:应用程序提交I/O请求后立即返回,并使用回调、轮询或事件通知等方式来处理操作完成的通知

        这四种I/O模型各有优劣,并适用于不同的应用场景:

  • 阻塞式I/O适用于简单的同步操作
  • 非阻塞式I/O适用于需要处理多个连接的场景
  • I/O多路复用适用于需要监视多个连接的场景
  • 异步I/O适用于需要高性能和扩展性的场景

        根据具体的应用需求和系统特点,可以选择合适的I/O模型来实现高效的数据传输和处理。

2. Java NIO

2.1 Java NIO概述

        在Java编程中,经常听到BIO,NIO,AIO等名词,这些名词一般指的是Java语言为实现不同类型的I/O操作所提供的API。

        1、Java IO:Java IO是JDK 1.0版本其自带的用于读取和写入数据的API。因其同步、阻塞的特征,被归类为同步阻塞式IO(Blocking IO),即Java BIO。

        2、Java NIO(New IO):是JDK 1.4开始提供的同步非阻塞式的IO操作API。因其同步、非阻塞的特征,开发者一般将Java NIO理解为Java Non-Blocking IO。

        3、Java NIO 2.0:是JDK 1.7开始提供的异步非阻塞式的IO操作API,因其是在NIO的基础上进行了改进,称为NIO 2.0。因其异步、非阻塞的特征,开发者一般将Java NIO 2.0称为Java AIO。

        下面以一个Web服务器的例子介绍Java BIO和Java NIO在实际使用中的差别。

        基于BIO的场景:

        基于NIO的场景:

2.2 Java BIO模型

        Java BIO和NIO采用了2种不同的模型。BIO使用了面向流(Stream Oriented)的模型。流(Stream)可以理解为从源节点到目标节点的数据通道,传输的数据像水流一样从源节点流向目标节点。

        面向流的特点:

  • 单向的:一个流中的数据仅能从一个方向流向另一个方向
  • 面向字节的:程序每次可以从流中读取1到多个字节,或写入1到多个字节
  • 无缓冲的:从流中读取数据后,流中的数据消失
  • 顺序访问:仅能按顺序逐个访问流中的数据,不能在流中的数据中前后移动

2.3 Java NIO模型

        Java NIO使用了面向缓冲区(Buffer Oriented)的,基于通道(Channel Based)的模型。模型的不同使得BIO和NIO在功能上和操作上有着不同的特点。

        在面向缓冲的模型中,数据通过数据通道(Channel)被读入/写入到一个缓冲区(Buffer)中,然后从中进行处理。

        可以使用一个生活中的例子来理解:现在需要从房间A搬运一些纸质文件到房间B,房间A和房间B之间通过一个走廊相连。搬运文件时,先将文件放到一个文件箱中,再搬着文件箱从A房间移动到B房间。

        在这个例子中,房间A和房间B分别是数据传输的起点和终点。起点和终点之间的走廊是Channel,临时存放纸质文件的文件箱是Buffer。

        面向缓冲的特点:

  • 双向的:通道可以用于读或写,也可以同时用于读写
  • 面向字节块:程序每次可以从缓冲区中获取一组字节数据
  • 缓冲的:缓冲中的数据可以被多次访问
  • 任意访问:允许在缓冲中的数据中前后移动

2.4 两种模型的对比

        Java BIO下的执行流程:

        Java NIO下的执行流程:

3. Java NIO API

3.1 Buffer

        Buffer(缓冲区)是Java NIO中提供的用于存储数据的容器。Buffer底层依靠数组来存储数据,并提供了对数据的结构化访问以及维护读写位置等信息的功能。

        Buffer只能用于存储基本类型的数据。在NIO中,针对八种基本类型提供了7个实现类。考虑到实际过程中,数据是以字节的形式来存储和传输,所以更多的使用的是ByteBuffer。

        Buffer是一个抽象类:

        其中定义了4个关于底层数组信息的核心属性:

  • capacity:容量位,用于标记该缓冲区的容量,缓冲区创建好之后不可变
  • position:操作位,用于指向要操作的位置,实际意义类似于数组中的下标,在缓冲区刚创建的时候指向0
  • limit:限制位,用于限制操作位position所能达到的最大位置。在缓冲区刚创建的时候指向容量位
  • mark:标记位,用于进行标记,在缓冲区刚创建的时候指向-1,默认不启用

        Buffer初始时的状态:

        示例代码:

        输出结果:

        Buffer写入部分数据后的状态:

        示例代码:

        输出结果:

        Buffer反转后的状态:

        示例代码:

        输出结果:

import java.nio.ByteBuffer;

public class BufferDemo {
    public static void main(String[] args) {
        //创建Buffer
        ByteBuffer buf = ByteBuffer.allocate(10);
        printBufferState(buf);
        // 写入数据到ByteBuffer
        String message = "Hello NIO";
        buf.put(message.getBytes());
        System.out.println("Before flip():");
        printBufferState(buf);
        // 反转缓冲区
        buf.flip();
        // 打印切换到读模式后的状态
        System.out.println("After flip():");
        printBufferState(buf);

        // 读取数据
        byte[] data = new byte[buf.limit()];
        buf.get(data);
        // 打印读取到的数据
        System.out.println("Read data: " + new String(data));
    }
    public static void printBufferState(ByteBuffer buf){
        //输出3个变量的值
        System.out.println("position="+buf.position()+",limit="+buf.limit()+",capacity="+buf.capacity());
    }
}

3.2 Channel

        Channel(通道)是Java NIO中提供的用于传输数据的工具,代表了源节点和目标节点之间的数据通道。Channel与Stream相似,但是略有不同:

  • Channel是双向的
  • Channel默认是阻塞的,可以设置为非阻塞
  • Channel是面向缓冲区操作的

        Java NIO中常用的Channel实现类如下:

        其中:

  • FileChannel:从文件读取数据和向文件读取数据
  • DatagramChannel:可以通过UDP协议在网络上读写数据
  • SocketChannel:可以通过TCP协议在网络上读写数据
  • ServerSocketChannel:允许您侦听传入的 TCP 连接,就像 Web 服务器一样。 对于每个传入连接,都会创建一个 SocketChannel
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
    public static void main(String[] args) throws Exception {
        readDemo();
        writeDemo();
    }
    public static void readDemo() throws Exception {
        RandomAccessFile aFile = new RandomAccessFile("data/hello.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(10);
        int bytesRead = inChannel.read(buf);
        while (bytesRead != -1) {
            System.out.println("\n====>Read " + bytesRead);
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char) buf.get()+" ");
            }
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        aFile.close();
    }

    public static void writeDemo() throws Exception {
        RandomAccessFile aFile = new RandomAccessFile("data/hello2.txt", "rw");
        FileChannel channel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.wrap("Hello Channel!".getBytes());
        int len=channel.write(buf);
        System.out.println("len="+len);
        aFile.close();
    }
} 

3.3 Selector

        Selector 是 Java NIO 提供的一个组件,它可以检查一个或多个Channel 实例,并确定哪些通道准备好用于读、写等操作。 通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接。

        Selector是基于事件驱动的,供提供了4类事件:connect、accept、read和write。

        这四类事件定义在SelectionKey中:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

        想要使用Selector来管理Channel,需要先向Selector注册该Channel实例,可以通过SelectableChannel.register()方法实现。

// 将channel设置为非阻塞模式
channel.configureBlocking(false);
// 将通道注册到选择器,并指定关注事件为读事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

        需要注意,Channel 必须处于非阻塞模式才能与 Selector 一起使用。 也就是说,不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 无法切换到非阻塞模式。SocketChannel是可以与Selector搭配使用的。

        创建一个ServerSocketChannel监听8080端口,并使用Selector来处理客户端的连接和数据读取。同时,创建了多个客户端线程,模拟并发访问。每个客户端线程会连接到服务器,并发送数据,然后接收服务器的响应。

        Server端程序代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class Server {
    public static void main(String[] args) throws IOException {
        // 创建Selector
        Selector selector = Selector.open();

        // 创建ServerSocketChannel并绑定端口
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false);

        // 将ServerSocketChannel注册到Selector上,并指定感兴趣的事件为接收连接事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server started.");

        while (true) {
            // Selector进行事件轮询
            selector.select();
            // 获取触发的事件集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    // 接收连接事件
                    handleAccept(key);
                } else if (key.isReadable()) {
                    // 可读事件
                    handleRead(key);
                }
            }
        }
    }

    private static void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(key.selector(), SelectionKey.OP_READ);
        System.out.println("Accepted new connection from: " + clientChannel.getRemoteAddress());
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);

        if (bytesRead == -1) {
            System.out.println("Connection closed by client: " + clientChannel.getRemoteAddress());
            // 客户端关闭连接
            clientChannel.close();
        } else if (bytesRead > 0) {
            // 处理接收到的数据
            buffer.flip();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            System.out.println("Received data from " + clientChannel.getRemoteAddress() + ": " + new String(data));

            // 回写响应数据
            String response = "Response from server";
            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
            clientChannel.write(responseBuffer);
        }
    }
}

        Client端程序代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread clientThread = new Thread(new ClientRunnable());
            clientThread.start();
        }
    }

    static class ClientRunnable implements Runnable {
        @Override
        public void run() {
            try {
                // 创建客户端SocketChannel并连接到服务器
                SocketChannel clientChannel = SocketChannel.open();
                clientChannel.configureBlocking(false);
                clientChannel.connect(new InetSocketAddress("localhost", 8080));

                // 等待连接完成
                while (!clientChannel.finishConnect()) {
                    Thread.sleep(100);
                }

                // 发送数据到服务器
                String message = "Hello from client";
                ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                clientChannel.write(buffer);

                // 接收服务器响应
                ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
                while (clientChannel.read(responseBuffer) <= 0) {
                    // 等待服务器响应
                    Thread.sleep(100);
                }
                responseBuffer.flip();
                byte[] responseData = new byte[responseBuffer.limit()];
                responseBuffer.get(responseData);
                System.out.println( clientChannel.getLocalAddress()+"=>Received response from server: " + new String(responseData));
                // 关闭客户端连接
                clientChannel.close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4. 总结

        1、阻塞(Blocking)和非阻塞(Nonblocking)描述的是线程在访问某个资源时,在该资源没有准备就绪期间的处理方式

  • 阻塞:阻塞是指在进行I/O操作时,当前线程会被挂起,等待数据的就绪或操作的完成
  • 非阻塞:非阻塞是指进行I/O操作时,当前线程不会被挂起,而是立即返回并继续执行其他任务

        2、同步(synchronous)与异步(asynchronous)描述的是线程在发出请求后,是否等待结果

  • 同步是指程序按照顺序执行,并等待某个操作完成后再继续执行下一个操作
  • 在异步操作中,程序不会阻塞等待结果的返回,而是通过回调、轮询或事件通知等机制来获取结果或处理完成的事件

        3、当涉及I/O操作时,可以根据是否阻塞和是否异步的角度将其分为以下四类

  • 阻塞式I/O(Blocking I/O)
  • 非阻塞式I/O(Non-Blocking I/O)
  • I/O多路复用(I/O Multiplexing)
  • 异步I/O(Asynchronous I/O)

        4、在Java编程中提到的BIO、NIO和AIO,一般指的是Java语言为实现不同类型的I/O操作所提供的API

  • Java IO:Java IO是JDK 1.0版本其自带的用于读取和写入数据的API,因其同步、阻塞的特征,被归类为同步阻塞式IO(Blocking IO),即Java BIO
  • Java NIO(New IO):是JDK 1.4开始提供的同步非阻塞式的IO操作API,因其同步、非阻塞的特征,开发者一般将Java NIO理解为Java Non-Blocking IO
  • Java NIO 2.0:是JDK 1.7开始提供的异步非阻塞式的IO操作API,因其是在NIO的基础上进行了改进,称为NIO 2.0;因其异步、非阻塞的特征,开发者一般将Java NIO 2.0称为Java AIO

        5、Java BIO和NIO采用了2种不同的模型

  • BIO使用了面向流(Stream Oriented)的模型
  • Java NIO使用了面向缓冲区(Buffer Oriented)的,基于通道(Channel Based)的模型

        6、Java NIO编程中的核心API包括Buffer、Channel和Selector

  • Buffer(缓冲区)是Java NIO中提供的用于存储数据的容器,底层依靠数组来存储数据,并提供了对数据的结构化访问以及维护读写位置等信息的功能
  • Channel(通道)是Java NIO中提供的用于传输数据的工具,代表了源节点和目标节点之间的数据通道
  • Selector 是 Java NIO 提供的一个组件,它可以检查一个或多个Channel 实例,并确定哪些通道准备好用于读、写等操作,通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接

网站公告

今日签到

点亮在社区的每一天
去签到