【Java核心-基础】IO

.

简述

Java 的 IO 方式大致可分为 3 类:BIO、NIO、NIO 2(AIO)

这 3 类 IO 方式在 同步/异步 和 阻塞/非阻塞 方面有明显区别。

同步 vs 异步

同步:后续任务必须等当前调用返回后再执行。

异步:后续任务不需要等待当前调用返回,依靠事件、回调等机制实现任务间次关系。

阻塞 vs 非阻塞

阻塞:执行阻塞操作时,当前线程会处于阻塞状态,无法执行其它任务,只有条件就绪后才能继续。

非阻塞:不管操作是否结束,当前线程直接返回,由后台其它线程继续处理。

 

 

BIO

【Java核心-基础】IO

BIO 指传统的 java.io 包,也包括 java.net 下的部分API(如,Socket、ServerSocket、HttpURLConnection)。

BIO 基于流模型,以同步、阻塞的方式交互(B:Blocking)。

优点:简单、直观。

缺点:IO 效率 和 扩展性 存在局限性,容易成为性能瓶颈。

 

  • InputStream / OutputStream 用于读写 字节

  • Reader / Writer 增加了编解码功能,用于读写 字符

  • BufferedInputStream / BufferedOutputStream 带缓存区,对批量数据进行一次操作,可以减少对磁盘等硬件的读写频率,提高IO效率

    写数据后需要 flush

  • Closeable 接口提供的 close() 方法可以释放相关资源

    如,释放 FileInputStream 所获取的文件描述符(FileDescriptor)

    可以利用 try-with-resourcestry-finally 机制来确保调用了 close()方法

 

NIO

Java 1.4 引入了 NIO

提供了 Channel、Selector、Buffer 等机制,可以构建多路复用、同步非阻塞的IO程序(N:New、Non-Blocking)。

它提供了更接近操作系统底层的高性能数据操作方式。

JDK 的这部分功能在不同操作系统上实现有较大不同,应在真实的运行环境上测试。如,开发环境是 Windows,部署环境是 Linux,则应以 Linux 环境的调试结果为准。

 

Java 1.7 引入了 NIO 2(AIO)

提供了异步非阻塞的IO方式(A:Asynchronous)。

 

缺点:单个请求中数据量较大时,对后续事件的响应会被延迟。所以多路复用适用于大量请求大小有限的场景。

 

  • Buffer 是高效的数据容器

    除 boolean 外,所有原始数据类型都有相应的 Buffer 实现。

  • Channel 用于支持 批量 式 IO

    类似 Linux 上的文件描述符,比 File、Socket 更接近操作系统底层。

    Channel 可以充分利用操作系统底层机制,获得特定场景的性能优化。如,DMA(Direct Memory Access)。

  • Selector 用于支持 多路复用

    它可以监测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,实现单线程对多 Channel 的高效管理。

    针对不同操作系统,Selector 的实现了不同;Linux 上依赖于 Epoll;Windows 上依赖于 iocp

 

BIO 和 NIO 应用差异示例

此处以 Client-Server Socket 通信为例来说明 NIO 的应用差异。

 

用 BIO 实现

针对每个来自 Client 的请求,都创建一个对应线程执行,或交由线程池处理。

缺点:扩展性差。大量客户端请求时,产生大量连接,服务端对每个连接的处理需要线程上下文切换,开销过高。

【Java核心-基础】IO

Java代码

 【Java核心-基础】IO

  1. ServerSocket serverSocket = new ServerSocket(80);  

  2. while (true) {  

  3.   Socket socket = serverSocket.accept();  

  4.   Thread requestHandler = new Thread(()->{  

  5.     // 处理来自客户端的请求(socket)  

  6.   });  

  7.   requestHandler.start();  

  8. }  

 

用 NIO 实现

每一个来自 Client 的请求都被汇聚到 Selector,用单线程轮询定位就绪的 Channel,再处理相应的请求。

Selector 类似于一个 调度员

  • 同步:每个准备好的 channel 处理是依次进行的;

  • 非阻塞:不需要每个连接(channel)都有一个对应的线程在等它就绪 。

    只有 select 阶段是阻塞的,可以避免大量客户端连接导致线程频繁切换的问题。

【Java核心-基础】IO

示例代码只注册了一个 Channel。

真实应用中一般会创建 多个 Channel,注册到同一个 Selector,也就是 多路复用。

为了避免单个 Selector (单线程)执行监听任务成为瓶颈,也可以尝试用 多个 Selector。

 

Java代码

 【Java核心-基础】IO

  1. try (Selector selector = Selector.open();  

  2.     ServerSocketChannel socketChannel = ServerSocketChannel.open()) {  

  3.   socketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 80));  

  4.   socketChannel.configureBlocking(false);  

  5.   // 注册到 Selector,并说明关注点(accept)  

  6.   socketChannel.register(selector, SelectionKey.OP_ACCEPT);  

  7.   while (true) {  

  8.     selector.select(); // 等待就绪的 Channel(这是阻塞操作)  

  9.     Set selectionKeys = selector.selectedKeys();  

  10.     Iterator iter = selectionKeys.iterator();  

  11.     while (iter.hasNext()) {  

  12.       SelectionKey key = iter.next();  

  13.       // 处理请求  

  14.       ServerSocketChannel channel = (ServerSocketChannel)key.channel();  

  15.       ...  

  16.       iter.remove();  

  17.     }  

  18.   }  

  19. } catch (IOException e) {  

  20.   // 处理异常  

  21. }  

 

用 AIO 实现

这只是个简单的示例,真实应用中会更复杂

Java代码

 【Java核心-基础】IO

  1. try {  

  2.   AsynchronousServerSocketChannel asyncChannel  

  3.       = AsynchronousServerSocketChannel.open();  

  4.   asyncChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 80));  

  5.   asyncChannel.accept(null,  

  6.       new CompletionHandler<AsynchronousSocketChannel, Void>() {  

  7.         @Override  

  8.         public void completed(AsynchronousSocketChannel result,  

  9.             Void attachment) {  

  10.           // 接收下一次连接  

  11.           asyncChannel.accept(null, this);  

  12.           // 处理本次连接  

  13.           ...  

  14.         }  

  15.   

  16.         @Override  

  17.         public void failed(Throwable exc, Void attachement) {  

  18.           // 处理连接失败的情况  

  19.         }  

  20.       })  

  21. } catch (IOException e) {  

  22.   // 处理异常  

  23. }