从Java IO到Java NIO:如何理解阻塞和非阻塞I/O的区别?

2023年 8月 5日 58.0k 0

Java NIO实现非阻塞I/O

在Java中,阻塞I/O(Blocking I/O)和非阻塞I/O(Non-blocking I/O)是两种不同的I/O模式。

阻塞I/O模式下,当应用程序进行输入/输出操作时,线程会一直阻塞,直到数据传输完成或者发生异常。在此期间,线程无法执行其他任务,因此阻塞I/O模式具有较低的效率和响应性能。

非阻塞I/O模式下,当应用程序进行输入/输出操作时,线程会立即返回,并且不会等待数据传输完成。在此期间,线程可以执行其他任务,因此非阻塞I/O模式具有较高的效率和响应性能。

Java NIO中的非阻塞I/O是基于选择器(Selector)和通道(Channel)的。选择器可以监听多个通道上的I/O事件,并在有事件发生时通知应用程序,从而实现非阻塞I/O操作。通道则是用于输入/输出操作的对象,可以是文件通道或网络通道。

Java NIO是非阻塞的,因为它基于选择器和通道实现了非阻塞I/O,支持同时处理多个通道的I/O事件,从而提高了I/O操作的效率和响应性能。相比之下,传统的Java IO(也称为IO流)是阻塞的,因为它只能同时处理一个输入/输出流,当进行输入/输出操作时,线程会一直阻塞,直到数据传输完成或者发生异常。

1、创建通道

通道是Java NIO中用于输入/输出操作的对象,可以通过SocketChannel、ServerSocketChannel、DatagramChannel等创建网络通道,或者通过FileChannel创建文件通道。在这里,我们以SocketChannel为例创建网络通道。

SocketChannel channel = SocketChannel.open();

2、将通道设置为非阻塞模式

通过调用通道的configureBlocking(false)方法,将通道设置为非阻塞模式。在非阻塞模式下,通道的读取和写入操作不会阻塞线程,而是立即返回。

channel.configureBlocking(false);

3、创建选择器

选择器是Java NIO中用于监听多个通道的I/O事件的对象,用于实现非阻塞I/O。可以通过Selector.open()方法创建选择器。

Selector selector = Selector.open();

4、将通道注册到选择器上

通过调用通道的register()方法,将通道注册到选择器上,并指定要监听的事件类型,例如读取事件、写入事件、连接事件、接受事件等。在这里,我们注册了读取事件。

channel.register(selector, SelectionKey.OP_READ);

5、轮询选择器

通过调用选择器的select()方法,轮询选择器上注册的通道,当有通道上的I/O事件就绪时,select()方法会返回就绪的通道数量。

while (true) {
    selector.select();
    Set selectedKeys = selector.selectedKeys();
    Iterator keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 处理就绪的通道
        keyIterator.remove();
    }
}

6、处理就绪的通道

通过调用选择器的selectedKeys()方法,获取所有就绪的通道,并进行相应的读取或写入操作。在这里,我们实现了从通道读取数据的操作。

while (true) {
    selector.select();
    Set selectedKeys = selector.selectedKeys();
    Iterator keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isReadable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);
            while (bytesRead > 0) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                buffer.clear();
                bytesRead = channel.read(buffer);
            }
            if (bytesRead == -1) {
                channel.close();
            }
        }
        keyIterator.remove();
    }
}

需要注意的是,在非阻塞I/O模式下,读取和写入操作通常需要多次调用,直到完整的数据传输完成。在读取操作中,需要将数据从通道读取到缓冲区,并判断缓冲区中是否已经读取完毕。

此外,在非阻塞I/O模式下,发生异常的可能性比较高,因此需要进行异常处理。可以通过选择器的selectedKeys()方法和SelectionKey的readyOps()方法,判断通道是否出现异常,并进行相应的处理。

以下是完整的示例代码。在这个例子中,我们使用了一个简单的Echo服务器,将客户端发送的消息原样返回。

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 NonBlockingServer {
    public static void main(String[] args) throws IOException {
        // 创建服务器套接字通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(9999));
        serverChannel.configureBlocking(false);

        // 创建选择器
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started on port 9999");

        while (true) {
            selector.select();
            Set selectedKeys = selector.selectedKeys();
            Iterator keyIterator = selectedKeys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if (key.isAcceptable()) {
                    // 处理连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = channel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Client connected: " + clientChannel.getRemoteAddress());
                } else if (key.isReadable()) {
                    // 处理读取事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = channel.read(buffer);
                    while (bytesRead > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            channel.write(buffer);
                        }
                        buffer.clear();
                        bytesRead = channel.read(buffer);
                    }
                    if (bytesRead == -1) {
                        channel.close();
                    }
                }
                keyIterator.remove();
            }
        }
    }
}

问题:selector.select()是阻塞,为什么还说NIO是非阻塞的呢?

selector.select()方法确实会阻塞,直到有至少一个通道准备好进行I/O操作或者等待超时或中断。但是,需要注意的是,这种阻塞只会影响当前的线程,不会影响应用程序的其他线程。

在服务端线程调用选择器的select()方法时,只有当前服务端线程会被阻塞,而不是客户端线程。

客户端的阻塞和非阻塞I/O操作取决于具体的实现。对于阻塞I/O模式,客户端线程在进行输入/输出操作时,会一直阻塞,直到数据传输完成或者发生异常。对于非阻塞I/O模式,客户端线程在进行输入/输出操作时,会立即返回,并且不会等待数据传输完成。在此期间,客户端线程可以执行其他任务。

因此,Java NIO仍然可以称为非阻塞I/O。

Java NIO提供了一种基于事件驱动的I/O模型,应用程序使用选择器(Selector)来注册通道(Channel)上的I/O事件,并在有事件发生时进行相应的处理。在选择器上调用select()方法会阻塞当前线程,直到至少有一个通道上注册的事件发生,此时select()方法会返回,应用程序可以通过selectedKeys()方法获取就绪的事件。由于选择器可以同时监听多个通道,因此Java NIO可以同时处理多个通道上的I/O事件,从而提高了I/O操作的效率和响应性能。

需要注意的是,虽然选择器的select()方法会阻塞当前线程,但是可以通过调用选择器的wakeup()方法中断阻塞,使得select()方法立即返回。此外,可以在选择器上设置超时时间,使得select()方法在指定时间内返回,避免长时间的无限阻塞。

实战Java NIO中实现文件I/O(File I/O)和网络I/O(Network I/O)

文件I/O(File I/O)

Java NIO中的文件I/O是通过FileChannel来实现的。FileChannel类提供了读取和写入文件的方法,而ByteBuffer类则用于存储读取和写入的数据。

以下是实现文件I/O的详细步骤:

步骤1:获取FileChannel实例

在进行文件I/O之前,需要先获取FileChannel实例。可以通过FileInputStream或FileOutputStream来获取FileChannel实例,例如:

FileInputStream fileInputStream = new FileInputStream("file.txt");
FileChannel fileChannel = fileInputStream.getChannel();

步骤2:创建ByteBuffer

在进行文件I/O之前,需要先创建ByteBuffer实例,用于存储读取和写入的数据。可以通过ByteBuffer的allocate方法创建ByteBuffer实例,例如:

ByteBuffer buffer = ByteBuffer.allocate(1024);

步骤3:读取文件数据

(1)从FileChannel中读取数据

可以通过FileChannel的read方法从文件中读取数据,并将数据存储到ByteBuffer中。read方法有两个重载版本:

int read(ByteBuffer dst) throws IOException;
long read(ByteBuffer[] dsts, int offset, int length) throws IOException;

第一个版本的read方法将数据读取到单个ByteBuffer中,返回值为读取的字节数。如果返回值为-1,表示已经读取到了文件的末尾。

第二个版本的read方法将数据读取到多个ByteBuffer中,返回值为读取的字节数。如果返回值为-1,表示已经读取到了文件的末尾。

以下是使用第一个版本read方法的示例代码:

int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
    buffer.flip();
    while (buffer.hasRemaining()) {
        System.out.print((char) buffer.get());
    }
    buffer.clear();
    bytesRead = fileChannel.read(buffer);
}

上述代码首先通过FileChannel的read方法将数据读取到ByteBuffer中,并返回读取的字节数。随后,通过flip方法将ByteBuffer从写模式切换为读模式,并通过get方法读取ByteBuffer中的数据。当ByteBuffer中的数据被读取完毕后,通过clear方法将ByteBuffer从读模式切换为写模式,并再次调用FileChannel的read方法读取文件中的数据,直到文件中的所有数据被读取完毕。

(2)向FileChannel中写入数据

可以通过FileChannel的write方法向文件中写入数据,例如:

byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = fileChannel.write(buffer);

上述代码首先将数据存储到ByteBuffer中,随后调用FileChannel的write方法将数据写入到文件中。

步骤4:关闭FileChannel

在使用完FileChannel后,需要调用其close方法关闭FileChannel,例如:

fileChannel.close();

完整的代码示例:

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileIODemo {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("file.txt");
        FileChannel fileChannel = fileInputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = fileChannel.read(buffer);
        while (bytesRead != -1) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear();
            bytesRead = fileChannel.read(buffer);
        }

        fileChannel.close();
    }
}

网络I/O(Network I/O)

Java NIO中的网络I/O是通过SocketChannel和ServerSocketChannel来实现的,它们分别用于客户端和服务端的网络通信。

以下是实现网络I/O的详细步骤:

步骤1:获取SocketChannel或ServerSocketChannel实例

在进行网络I/O之前,需要先获取SocketChannel或ServerSocketChannel实例。可以通过SocketChannel或ServerSocketChannel的open方法获取相应的实例,例如:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.example.com", 80));

或:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));

步骤2:创建ByteBuffer

在进行网络I/O之前,需要先创建ByteBuffer实例,用于存储读取和写入的数据。可以通过ByteBuffer的allocate方法创建ByteBuffer实例,例如:

ByteBuffer buffer = ByteBuffer.allocate(1024);

步骤3:读取网络数据

(1)从SocketChannel中读取数据

可以通过SocketChannel的read方法从网络中读取数据,并将数据存储到ByteBuffer中。read方法的用法与文件I/O中的read方法相同,这里不再赘述。

以下是使用SocketChannel的read方法的示例代码:

int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
    buffer.flip();
    while (buffer.hasRemaining()) {
        System.out.print((char) buffer.get());
    }
    buffer.clear();
    bytesRead = socketChannel.read(buffer);
}

上述代码首先通过SocketChannel的read方法将数据读取到ByteBuffer中,并返回读取的字节数。随后,通过flip方法将ByteBuffer从写模式切换为读模式,并通过get方法读取ByteBuffer中的数据。当ByteBuffer中的数据被读取完毕后,通过clear方法将ByteBuffer从读模式切换为写模式,并再次调用SocketChannel的read方法读取网络中的数据,直到网络中的所有数据被读取完毕。

(2)向SocketChannel中写入数据

可以通过SocketChannel的write方法向网络中写入数据,例如:

byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = socketChannel.write(buffer);

上述代码首先将数据存储到ByteBuffer中,随后调用SocketChannel的write方法将数据写入到网络中。

步骤4:关闭SocketChannel或ServerSocketChannel

在使用完SocketChannel或ServerSocketChannel后,需要调用其close方法关闭SocketChannel或ServerSocketChannel,例如:

socketChannel.close();

或:

serverSocketChannel.close();

:完整的代码示例:

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

public class NetworkIODemo {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("www.example.com", 80));

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = socketChannel.read(buffer);
        while (bytesRead != -1) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear();
            bytesRead = socketChannel.read(buffer);
        }

        socketChannel.close();
    }
}

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论