面试官:Java Nio的优缺点?可能的瓶颈有哪些?

了解NIO代码语言:javascript复制 Java NIO(New Input/Output)是Java提供的一种新的I/O(输入/输出)模型,引入了基于通道(Channel)和缓冲区(Buffer)的概念,相比传统的流式I/O模型,提供了更高效、更灵活的I/O操作方式。NIO中的 N 可以理解为 Non-blocking,同步非阻塞IO。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。

NIO 这两种通道都支持阻塞和非阻塞两种模式。

阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式有较好的性能和可靠性。

应用场景Java NIO 提供了一种更高效、更灵活的方式来处理I/O操作

因此特别适用于高并发、大规模数据处理的场景。它在网络编程、文件处理、多媒体处理等领域都有广泛的应用。

核心概念Java NIO 的核心概念包括以下几个部分:

通道(Channel):

通道是数据传输的载体,可以与文件或网络套接字建立连接。通道提供了双向的数据传输能力,可以读取和写入数据。Java NIO 中的所有 I/O 操作都是通过通道进行的。

缓冲区(Buffer):

缓冲区是一块连续的内存区域,用于存储数据。缓冲区提供了对数据的结构化访问,可以方便地读取和写入数据。数据通过缓冲区进行传输,从通道Channel读取到缓冲区,从缓冲区写入到通道Channel。

选择器(Selector):

选择器是用于多路复用 I/O 的组件,可以通过一个线程管理多个通道。选择器可以同时监控多个通道上的事件,如读取事件、写入事件等。通过选择器,可以实现单线程处理多个通道的I/O操作。

Nio的常用方式 对于NIO来说selector主要用来接受客户端的连接,所以一般用在server端。我们以一个NIO的服务器端和客户端聊天室为例来讲解NIO在JDK中是怎么使用的。

我们选择Socket协议为基础的ServerSocketChannel

首先就是open这个Server channel:

代码语言:javascript复制ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.bind(new InetSocketAddress("localhost", 9090));

serverSocketChannel.configureBlocking(false);然后向server channel中注册selector:

代码语言:javascript复制Selector selector = Selector.open();

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);虽然是NIO,但是对于Selector来说,它的select方法是阻塞方法,

只有找到匹配的channel之后才会返回,为了多次进行select操作,我们需要在一个while循环里面进行selector的select操作:

代码语言:javascript复制while (true) {

selector.select();

Set selectedKeys = selector.selectedKeys();

Iterator iter = selectedKeys.iterator();

while (iter.hasNext()) {

SelectionKey selectionKey = iter.next();

if (selectionKey.isAcceptable()) {

register(selector, serverSocketChannel);

}

if (selectionKey.isReadable()) {

serverResponse(byteBuffer, selectionKey);

}

iter.remove();

}

Thread.sleep(1000);

}selector中会有一些SelectionKey,SelectionKey中有一些表示操作状态的OP Status,根据这个OP Status的不同,selectionKey可以有四种状态,分别是isReadable,isWritable,isConnectable和isAcceptable。

当SelectionKey处于isAcceptable状态的时候,表示ServerSocketChannel可以接受连接了,我们需要调用register方法将serverSocketChannel accept生成的socketChannel注册到selector中,以监听它的OP READ状态,后续可以从中读取数据:

代码语言:javascript复制private static void register(Selector selector, ServerSocketChannel serverSocketChannel)

throws IOException {

SocketChannel socketChannel = serverSocketChannel.accept();

socketChannel.configureBlocking(false);

socketChannel.register(selector, SelectionKey.OP_READ);

}当selectionKey处于isReadable状态的时候,表示可以从socketChannel中读取数据然后进行处理:

代码语言:javascript复制private static void serverResponse(ByteBuffer byteBuffer, SelectionKey selectionKey)

throws IOException {

SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

socketChannel.read(byteBuffer);

byteBuffer.flip();

byte[] bytes= new byte[byteBuffer.limit()];

byteBuffer.get(bytes);

log.info(new String(bytes).trim());

if(new String(bytes).trim().equals(BYE_BYE)){

log.info("BYE");

socketChannel.write(ByteBuffer.wrap("BYE".getBytes()));

socketChannel.close();

}else {

socketChannel.write(ByteBuffer.wrap("BYE—BYE".getBytes()));

}

byteBuffer.clear();

} 上面的serverResponse方法中,从selectionKey中拿到对应的SocketChannel,然后调用SocketChannel的read方法,将channel中的数据读取到byteBuffer中,要想回复消息到channel中,还是使用同一个socketChannel,然后调用write方法回写消息给client端,到这里一个简单的回写客户端消息的server端就完成了。

接下来就是对应的NIO客户端,在NIO客户端需要使用SocketChannel,首先建立和服务器的连接:

代码语言:javascript复制socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090));然后就可以使用这个channel来发送和接受消息了:

代码语言:javascript复制public String sendMessage(String msg) throws IOException {

byteBuffer = ByteBuffer.wrap(msg.getBytes());

String response = null;

socketChannel.write(byteBuffer);

byteBuffer.clear();

socketChannel.read(byteBuffer);

byteBuffer.flip();

byte[] bytes= new byte[byteBuffer.limit()];

byteBuffer.get(bytes);

response =new String(bytes).trim();

byteBuffer.clear();

return response;

} 向channel中写入消息可以使用write方法,从channel中读取消息可以使用read方法。

这样一个NIO的客户端就完成了。

虽然以上是NIO的server和client的基本使用,但是基本上涵盖了NIO的所有要点

优缺点及瓶颈优点非阻塞:Java NIO使用了非阻塞I/O模型,允许应用程序在进行I/O操作时不必等待,可以继续处理其他任务,提高了系统的并发性能。内存效率:Java NIO使用了直接内存缓冲区(Direct Buffer),可以直接操作系统内存,避免了数据在Java堆和操作系统之间的复制,提高了内存使用效率 多路复用:有selector多路复用,因此1个线程就行就能处理所有连接,这个线程不停循环遍历处理channel,可以通过一个线程同时监视多个通道的状态,从而实现单线程处理多个I/O操作。缺点学习曲线:对于初学者来说,理解和使用Java NIO的概念和 API 可能需要一定的学习曲线,相对于传统的Java I/O,需要更多的学习和实践。复杂性:相比传统的Java I/O,Java NIO的编程模型更加复杂,需要处理事件驱动和回调机制,编写的代码可能更加复杂和难以理解。性能方面:单线程不停循环发起系统调用,一样会耗尽 CPU 资源。NIO 可能的瓶颈使用内存占用:使用直接内存缓冲区可能导致较高的内存消耗,特别是在处理大量数据时,对于内存的管控需要更加严格。复杂性导致的错误:由于Java NIO的复杂性,编写代码时可能容易出错,例如处理错误的事件、资源泄漏等