java面试题之----IO与NIO的区别

JAVA NIO vs IO

当我们学习了Java NIO和IO后,我们很快就会思考一个问题:

什么时候应该使用IO,什么时候我应该使用NIO

在下文中我会尝试用例子阐述java NIO 和IO的区别,以及它们对你的设计会有什么影响

Java NIO和IO的主要区别

 

IO NIO
面向Stream 面向Buffer
阻塞IO 非阻塞IO
  Selectors

 

面向Stream和面向Buffer

Java NIO和IO之间最大的区别是IO是面向流(Stream)的,NIO是面向块(buffer)的,所以,这意味着什么?

面向流意味着从流中一次可以读取一个或多个字节,拿到读取的这些做什么你说了算,这里没有任何缓存(这里指的是使用流没有任何缓存,接收或者发送的数据是缓存到操作系统中的,流就像一根水管从操作系统的缓存中读取数据)而且只能顺序从流中读取数据,如果需要跳过一些字节或者再读取已经读过的字节,你必须将从流中读取的数据先缓存起来

面向块的处理方式有些不同,数据是先被 读/写到buffer中的,根据需要你可以控制读取什么位置的数据。这在处理的过程中给用户多了一些灵活性然而,你需要额外做的工作是检查你需要的数据是否已经全部到了buffer中,你还需要保证当有更多的数据进入buffer中时,buffer中未处理的数据不会被覆盖

阻塞IO和非阻塞IO

所有的Java IO流都是阻塞的,这意味着,当一条线程执行read()或者write()方法时,这条线程会一直阻塞知道读取到了一些数据或者要写出去的数据已经全部写出,在这期间这条线程不能做任何其他的事情

java NIO的非阻塞模式(Java NIO有阻塞模式和非阻塞模式,阻塞模式的NIO除了使用Buffer存储数据外和IO基本没有区别)允许一条线程从channel中读取数据,通过返回值来判断buffer中是否有数据,如果没有数据,NIO不会阻塞,因为不阻塞这条线程就可以去做其他的事情,过一段时间再回来判断一下有没有数据

NIO的写也是一样的,一条线程将buffer中的数据写入channel,它不会等待数据全部写完才会返回,而是调用完write()方法就会继续向下执行

Selectors

Java NIO的selectors允许一条线程去监控多个channels的输入,你可以向一个selector上注册多个channel,然后调用selector的select()方法判断是否有新的连接进来或者已经在selector上注册时channel是否有数据进入。selector的机制让一个线程管理多个channel变得简单。


NIO和IO对应用的设计有何影响

选择使用NIO还是IO做你的IO工具对应用主要有以下几个方面的影响

1、使用IO和NIO的API是不同的(废话)

2、处理数据的方式

3、处理数据所用到的线程数

处理数据的方式

在IO的设计里,要一个字节一个字节从InputStream 或者Reader中读取数据,想象你正在处理一个向下面的基于行分割的流

 

  1.  
    Name:Anna
  2.  
    Age: 25
  3.  
    Email: anna@mailserver.com
  4.  
    Phone:1234567890

处理文本行的流的代码应该向下面这样

 

  1. InputStream input = ... ; // get the InputStream from the client socket
  2.  
  3. BufferedReader reader = new BufferedReader(new InputStreamReader(input));
  4.  
  5. String nameLine = reader.readLine();
  6. String ageLine = reader.readLine();
  7. String emailLine = reader.readLine();
  8. String phoneLine = reader.readLine();

注意,一旦reader.readLine()方法返回,你就可以确定整行已经被读取,readLine()阻塞知道一整行都被读取

NIO的实现会有一些不同,下面是一个简单的例子

 

  1. ByteBuffer buffer = ByteBuffer.allocate(48);
  2.  
  3. int bytesRead = inChannel.read(buffer);

注意第二行从channel中读取数据到ByteBuffer,当这个方法返回你不知道是否你需要的所有数据都被读到buffer了,你所知道的一切就是有一些数据被读到了buffer中,但是你并不知道具体有多少数据,这使程序的处理变得稍微有些困难

想象一下,调用了read(buffer)方法后,只有半行数据被读进了buffer,例如:“Name: An”,你能现在就处理数据吗?当然不能。你需要等待直到至少一整行数据被读到buffer中,在这之前确保程序不要处理buffer中的数据

你如何知道buffer中是否有足够的数据可以被处理呢?你不知道,唯一的方法就是检查buffer中的数据。可能你会进行几次无效的检查(检查了几次数据都不够进行处理),这会令程序设计变得比较混乱复杂

 

  1. ByteBuffer buffer = ByteBuffer.allocate(48);
  2.  
  3. int bytesRead = inChannel.read(buffer);
  4.  
  5. while(! bufferFull(bytesRead) ) {
  6. bytesRead = inChannel.read(buffer);
  7. }

bufferFull方法负责检查有多少数据被读到了buffer中,根据返回值是true还是false来判断数据是否够进行处理。bufferFull方法扫描buffer但不能改变buffer的内部状态

is-data-in-buffer-ready 循环柱状图如下

总结

NIO允许你用一个单独的线程或几个线程管理很多个channels(网络的或者文件的),代价是程序的处理和处理IO相比更加复杂

如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些,相似的,如果你需要保持很多个到其他电脑的连接,例如P2P网络,用一个单独的线程来管理所有出口连接是比较合适的

 


如果你只有少量的连接但是每个连接都占有很高的带宽,同时发送很多数据,传统的IO会更适合


 

          NIO图解

                         

 

自我总结:
在 Java 中,IO(Input/Output)和 NIO(New Input/Output)是两种不同的输入输出处理方式,它们在设计理念、工作模式、性能等方面存在明显区别,下面详细介绍并举例说明。

1. 设计理念

  • IO:是面向流(Stream - Oriented)的,数据的读取和写入是按顺序进行的,像水流一样,从一端流向另一端。在 IO 中,有输入流(InputStream)和输出流(OutputStream),数据只能单向流动,读取和写入操作是阻塞的。
  • NIO:是面向缓冲区(Buffer - Oriented)和通道(Channel)的。数据会先被读取到缓冲区中,之后可以在缓冲区中对数据进行操作,通道则负责在缓冲区和数据源(如文件、网络套接字)之间传输数据。NIO 支持非阻塞操作,提高了系统的并发性能。

2. 阻塞与非阻塞

  • IO:是阻塞式 IO。当进行读写操作时,线程会被阻塞,直到数据读写完成。这意味着在数据传输过程中,线程无法执行其他任务,造成资源浪费。
  • NIO:支持非阻塞 IO。线程在进行读写操作时,如果数据还未准备好,线程不会被阻塞,可以继续执行其他任务,等数据准备好后再进行处理。

3. 选择器(Selector)

  • IO:没有选择器的概念,每个连接都需要一个独立的线程来处理,当连接数量增多时,会消耗大量的系统资源。
  • NIO:引入了选择器(Selector)的概念。一个选择器可以管理多个通道,通过选择器可以监控多个通道的读写状态,当某个通道有数据可读或可写时,选择器会通知线程进行相应的处理,从而实现单线程管理多个连接,减少了线程的创建和切换开销。

4. 示例代码对比

传统 IO 示例(文件复制)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class IOExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("input.txt");
             FileOutputStream fos = new FileOutputStream("output.txt")) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            // 从输入流读取数据到缓冲区
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 将缓冲区的数据写入输出流
                fos.write(buffer, 0, bytesRead);
            }
            System.out.println("文件复制完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,FileInputStreamFileOutputStream 是面向流的,read()write() 方法是阻塞的,线程会一直等待数据的读取和写入完成。

NIO 示例(文件复制)

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class NIOExample {
    public static void main(String[] args) {
        Path inputPath = Paths.get("input.txt");
        Path outputPath = Paths.get("output.txt");

        try (FileChannel inChannel = FileChannel.open(inputPath, StandardOpenOption.READ);
             FileChannel outChannel = FileChannel.open(outputPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 从输入通道读取数据到缓冲区
            while (inChannel.read(buffer) != -1) {
                buffer.flip();
                // 将缓冲区的数据写入输出通道
                outChannel.write(buffer);
                buffer.clear();
            }
            System.out.println("文件复制完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,使用了 FileChannelByteBufferFileChannel 是通道,用于在文件和缓冲区之间传输数据,ByteBuffer 是缓冲区,用于存储数据。通过 flip()clear() 方法可以对缓冲区进行读写模式的切换。

NIO 非阻塞示例(简单的网络服务器)

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 NIONonBlockingServer {
    public static void main(String[] args) {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                selector.select();
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = socketChannel.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            System.out.println("Received: " + new String(data));
                        }
                    }
                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,使用了 Selector 来监控 ServerSocketChannelSocketChannel 的状态。当有新的连接请求(OP_ACCEPT)或有数据可读(OP_READ)时,选择器会通知线程进行相应的处理,线程在等待过程中不会被阻塞,可以继续执行其他任务。

总结

传统 IO 是面向流的、阻塞式的,适用于连接数较少且数据传输量较小的场景;NIO 是面向缓冲区和通道的,支持非阻塞操作,引入了选择器,适用于高并发、连接数较多的场景,能有效提高系统的性能和资源利用率。

posted @ 2019-02-23 17:59  皇问天  阅读(1253)  评论(0)    收藏  举报