Java NIO总结

简介: Java NIO总结

1. IO和NIO

分类

阻塞

选择器

处理方式

读取方向

java.io

面向字节流、字符流

单向移动

java.nio

面向缓冲

可在缓冲区前后双向移动


1.1 阻塞 vs 非阻塞


Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
IO的各种流是阻塞的,就是当一个线程调用读写方法时,该线程会被阻塞,直到读写完,在这期间该线程不能干其他事,CPU转而去处理其他线程,假如一个线程监听一个端口,一天只会有几次请求进来,但是CPU却不得不为该线程不断的做上下文切换,并且大部分切换以阻塞告终。
NIO通讯是将整个任务切换成许多小任务,由一个线程负责处理所有IO事件,并负责分发。它是利用事件驱动机制,而不是监听机制,事件到的时候再触发。NIO线程之间通过wait,notify等方式通讯。保证了每次上下文切换都有意义,减少无谓的进程切换。


1.2 面向流 vs 面向缓冲


Java IO和NIO之间第一个最大的区别是,IO是面向流的NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。


2. 知识准备


2.1 缓冲区操作


网络异常,图片无法展示
|


  • (1)发起**read()**方法,执行系统调用将控制权移交给内核空间
  • (2)内核空间收到请求先在内核空间的RAM内存中查找数据,若存在则拷贝传输到用户空间的缓冲区,若不存在执行步骤(3),挂起请求进程,即发生阻塞等待
  • (3)从磁盘介质中读取数据放入RAM内存,拷贝传输到用户空间的缓冲区


为何不直接将磁盘介质拷贝传输到用户空间的缓冲区?


  1. 操作系统不允许用户空间直接访问硬件
  2. 内核空间作为中介者和桥梁,处理用户空间与底层硬件交互,对I/O数据进行分解、组合等


2.2 内核空间与用户空间


2.2.1 设计初衷


网络异常,图片无法展示
|


  • 对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃掉。
  • 对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行。
  • 现代的操作系统大都通过内核空间和用户空间的设计来保护操作系统自身的安全性和稳定性


2.2.2 空间态切换


当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态

网络异常,图片无法展示
|


发生从用户态内核态切换,有以下三种情况:

情况

发起

描述

发生系统调用时

主动

这是处于用户态的进程主动请求切换到内核态的一种方式。用户态的进程通过系统调用申请使用操作系统提供的系统调用服务例程来处理任务。而系统调用的机制,其核心仍是使用了操作系统为用户特别开发的一个中断机制来实现的,即软中断。

产生异常时

被动

当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行的进程切换到处理此异常的内核相关的程序中,也就是转到了内核态,如缺页异常。

外设产生中断时

被动

当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作的完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。


2.3 虚拟内存


网络异常,图片无法展示
|

所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件
RAM)内存地址。这样做好处颇多,总结起来可分为两大类:


  • 一个以上的虚拟地址可指向同一个物理内存地址。
  • 虚拟内存空间可大于实际可用的硬件内存。


2.4 分页技术


参考《Java IO》介绍,深入了解可以翻翻计算机组成原理相关书籍关于内存介绍


为了支持虚拟内存的第二个特性(寻址空间大于物理内存),就必须进行虚拟内存分页(经常称为交换,虽然真正的交换是在进程层面完成,而非页层面)。依照该方案,虚拟内存空间的页面能够继续存在于外部磁盘存储,这样就为物理内存中的其他虚拟页面腾出了空间。从本质上说,物理内存充当了分页区的高速缓存;而所谓分页区,即从物理内存置换出来,转而存储于磁盘上的内存页面。

网络异常,图片无法展示
|

把内存页大小设定为磁盘块大小的倍数,这样内核就可直接向磁盘控制硬件发布命令,把内存页写入磁盘,在需要时再重新装入。结果是,所有磁盘 I/O 都在页层面完成。对于采用分页技术的现代操作系统而言,这也是数据在磁盘与物理内存之间往来的唯一方式。


2.5 内存映射文件


内存映射文件是OS提供的一种无需用户态、内核态之间数据拷贝的对文件进行映射操作的机制,极大的提高了IO处理效率,可以参考我的另一篇文章整理 mmap内存映射原理


3. java.nio组成


java.nio主要通过Buffer(缓冲)、Channel(通道)、Selector(选择器) 实现组成


3.1 Buffer


参考我的另一篇文章整理,ByteBuffer总结


3.2 Channel


Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据,可以把通道理解为是访问IO服务的导管或者工具。
一般我们会把IO广义的分为 File IOStream IO,对应的是文件流(File)套接字流(Socket) ,因此通道也可以根据两种IO进行分配,一般分为


  • FileChannel 处理文件流的通道
  • ServerSocketChannel、SocketChannel、DatagramChannel 处理Socket套接字流的通道


java.nio.channels.spi 提供了可扩展的方式,允许新通道实现以一种受控且模块化的方式被植入到Java 虚拟机上专为某种操作系统、文件系统或应用程序而优化的通道来使性能最大化。


网络异常,图片无法展示
|


打开通道


  • 文件流只能通过RandomAccessFile、FileInputStreamFileOutputStream
    对象上调用getChannel( ) 方法来获取


RandomAccessFile raf = new RandomAccessFile ("somefile", "r");

FileChannel fc = raf.getChannel( );


  • 套接字流可以调用静态方法open() 来获取


SocketChannel sc = SocketChannel.open( );

sc.connect (new InetSocketAddress ("somehost", someport));

ServerSocketChannel ssc = ServerSocketChannel.open( );

ssc.socket( ).bind (new InetSocketAddress (somelocalport));

DatagramChannel dc = DatagramChannel.open( );


双向通道


ReadableByteChannel实现接口则为可读、WritableByteChannel实现接口则为可写,当只实现一个接口时,channel为单向的,ByteChannel实现了以上两个接口,因此实现ByteChannel接口的类是支持可读可写的,是双向通道
SelectableChannel实现接口则可以支持配合Selector(选择器) 使用


阻塞模式


可以设置阻塞(blocking)非阻塞(non-blocking) 模式


将非阻塞I/O 和选择器组合起来可以使您的程序利用多路复用 I/O(multiplexed I/O)


关闭通道


调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。


文件通道(FileChannel)


  • 文件通道总是阻塞式,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机
    制,使得本地磁盘 I/O 操作延迟很少
  • 对于文件 I/O,最强大之处在于异步 I/O(asynchronous I/O) ,它允许一个进程可以
    从操作系统请求一个或多个 I/O 操作而不必等待这些操作的完成


套接字通道(SocketChannel、ServerSocketChannel)


Socket通道可以选择阻塞非阻塞模式


SocketChannel 模拟连接导向的流协议(如 TCP/IP)
DatagramChannel 模拟包导向的无连接协议(如 UDP/IP)


流Socket数据报Socket对比:


  • UDP面向数据包,而TCP面向连接
  • UDP传输不可靠且不能保证有序,会产生数据丢失或无序的数据;TCP传输可靠有序,有重试机制保证
  • UDP“发射后不管”(fire and forget)而不需要知道您发送的包是否已接收
  • UDP数据吞吐量更高;TCP需要多次交互和机制保证相比会占用很多网络资源
  • UDP可以发送数据给多个接受者(多播或者广播)


关于Socket的IO介绍和使用可以参考我的另一篇文章 Socket通信原理及模型实现


管道(Pipe)


管道,即一对循环的通道。Pipe 类定义了两个嵌套的通道类来实现管路。


  • Pipe.SourceChannel(管道负责读的一端)source 通道类似java.io.PipedInputStream提供的功能
  • Pipe.SinkChannel(管道负责写的一端) sink 通道类似java.io.PipedOutputStream提供的功能


3.3 Selector


网络异常,图片无法展示
|

选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能。就像在第一章中描述的那样,就绪选择和多元执行使得单线程能够有效率地同时管理多个 I/O 通道(Channel)。
网络异常,图片无法展示
|

如果将一个Channel通道和事件Event注册到一个Selector上,那么Channel和Event在Selector上的映射关系组成了SelectonKey,也可以如上图将Channel注册到多个Selector上。


Selector


网络异常,图片无法展示
|

Selector 是Java nio能够支持高并发数据处理一个关键,其核心理念就是IO多路复用的原理,简单的说就是当多个客户端(Channel)连接服务器时,可以通过Selector同时对这些客户端请求进行监听,当客户端发送数据到服务器之后由Selector对这些Channel进行分发处理


SelectionKey


SelectionKey维护的是一个实现SelectableChannel的通道与Selector的映射关系,也就是多路复用器要监听哪些通道事件。


监听事件定义了如下四个:


  • OP_READ 读取
  • OP_WRITE 写入
  • OP_CONNECT 连接
  • OP_ACCEPT 接受


Selector代码示例


/**

* 单线程处理 多路复用

* created by guanjian on 2021/1/12 9:09

*/

public class SingleThreadNIOSocketChannelSelectorServer {


   public static void main(String[] args) throws IOException, InterruptedException {

       // 创建ServerSocketChannel通道,绑定监听端口为8080

       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

       serverSocketChannel.socket().bind(new InetSocketAddress(9999));

       // 设置为非阻塞模式

       serverSocketChannel.configureBlocking(false);

       // 注册选择器,设置选择器选择的操作类型

       Selector selector = Selector.open();

       serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


       System.out.println("Server starting ...");


       while (true) {

           System.out.println("Server receive request ...");

           // 等待请求,每次等待阻塞3s,超过时间则向下执行,若传入0或不传值,则在接收到请求前一直阻塞

           if (selector.select(1000) > 0) {

               System.out.println("Server receive event ...");

               // 获取待处理的选择键集合

               Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

               while (keyIterator.hasNext()) {

                   SelectionKey selectionKey = keyIterator.next();


                   // 如果是连接请求,调用处理器的连接处理方法

                   if (selectionKey.isAcceptable()) {

                       System.out.println("Server receive connect ...");

                       handleAccept(selectionKey);

                   }

                   // 如果是读请求,调用对应的读方法

                   if (selectionKey.isReadable()) {

                       System.out.println("Server receive read ...");

                       handleRead(selectionKey);

                   }

                   // 处理完毕从待处理集合移除该选择键

                   keyIterator.remove();

               }

           }

           //为了打印日志,故意设置时间间隔

           Thread.sleep(2000);

       }


   }


   public static void handleAccept(SelectionKey selectionKey) throws IOException {

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

       socketChannel.configureBlocking(false);

       socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(1024));

   }


   public static void handleRead(SelectionKey selectionKey) throws IOException {

       // 获取套接字通道

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

       // 获取缓冲器并进行重置,selectionKey.attachment()为获取选择器键的附加对象

       ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();

       byteBuffer.clear();

       // 没有内容则关闭通道

       if (socketChannel.read(byteBuffer) == -1) {

           socketChannel.close();

       } else {

           // 将缓冲器转换为读状态

           byteBuffer.flip();

           // 将缓冲器中接收到的值按localCharset格式编码保存

           String receivedRequestData = Charset.forName("UTF-8").newDecoder().decode(byteBuffer).toString();

           System.out.format("Server receive data:" + receivedRequestData);

           // 关闭通道

           //socketChannel.close();

       }

   }

}


参考


https://zhuanlan.zhihu.com/p/56876443
https://www.cnblogs.com/aspirant/p/8630283.html
https://www.cnblogs.com/sparkdev/p/8410350.html
https://www.cnblogs.com/vampirem/p/3157612.html
https://www.cnblogs.com/zhya/p/9640016.html
《Java NIO》

相关文章
|
3天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
17 3
|
11天前
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
27 5
|
5月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
1月前
|
消息中间件 缓存 Java
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
零拷贝技术 Zero-Copy 是指计算机执行操作时,可以直接从源(如文件或网络套接字)将数据传输到目标缓冲区, 而不需要 CPU 先将数据从某处内存复制到另一个特定区域,从而减少上下文切换以及 CPU 的拷贝时间。
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
|
2月前
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
38 2
|
3月前
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
Java NIO 开发
|
4月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
99 2
|
4月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
155 0
|
5月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
191 1
|
4月前
|
存储 网络协议 Java
【Netty 神奇之旅】Java NIO 基础全解析:从零开始玩转高效网络编程!
【8月更文挑战第24天】本文介绍了Java NIO,一种非阻塞I/O模型,极大提升了Java应用程序在网络通信中的性能。核心组件包括Buffer、Channel、Selector和SocketChannel。通过示例代码展示了如何使用Java NIO进行服务器与客户端通信。此外,还介绍了基于Java NIO的高性能网络框架Netty,以及如何用Netty构建TCP服务器和客户端。熟悉这些技术和概念对于开发高并发网络应用至关重要。
94 0