文档章节

深入理解JAVA中的NIO

OSC_烂猪皮
 OSC_烂猪皮
发布于 07/09 21:48
字数 3583
阅读 3410
收藏 121
点赞 7
评论 15

前言:

传统的 IO 流还是有很多缺陷的,尤其它的阻塞性加上磁盘读写本来就慢,会导致 CPU 使用效率大大降低。

所以,jdk 1.4 发布了 NIO 包,NIO 的文件读写设计颠覆了传统 IO 的设计,采用通道+缓存区使得新式的 IO 操作直接面向缓存区,并且是非阻塞的,对于效率的提升真不是一点两点,我们一起来看看。

通道 Channel

我们说过,NIO 的核心就是通道和缓存区,所以它们的工作模式是这样的:

通道有点类似 IO 中的流,但不同的是,同一个通道既允许读也允许写,而任意一个流要么是读流要么是写流。

但是你要明白一点,通道和流一样都是需要基于物理文件的,而每个流或者通道都通过文件指针操作文件,这里说的「通道是双向的」也是有前提的,那就是通道基于随机访问文件『RandomAccessFile』的可读可写文件指针。

『RandomAccessFile』是既可读又可写的,所以基于它的通道是双向的,所以,「通道是双向的」这句话是有前提的,不能断章取义。

基本的通道类型有如下一些:

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

FileChannel 是基于文件的通道,SocketChannel 和 ServerSocketChannel 用于网络 TCP 套接字数据报读写,DatagramChannel 是用于网络 UDP 套接字数据报读写。

通道不能单独存在,它永远需要绑定一个缓存区,所有的数据只会存在于缓存区中,无论你是写或是读,必然是缓存区通过通道到达磁盘文件,或是磁盘文件通过通道到达缓存区。

即缓存区是数据的「起点」,也是「终点」,具体这些通道到底有哪些不同以及该如何使用,基本实现如何,我们介绍完『缓存区』概念后,再做详细学习。

缓存区 Buffer

Buffer 是所有具体缓存区的基类,是一个抽象类,它的实现类有很多,包含各种类型数据的缓存。

  1. ByteBuffer

  2. CharBuffer

  3. ShortBuffer

  4. IntBuffer

  5. LongBuffer

  6. FloatBuffer

  7. DoubleBuffer

  8. MappedByteBuffer

我们以 ByteBuffer 为例进行学习,其余的缓存区也都是基于字节缓存区的,只不过多了一步字节转换过程而已,MappedByteBuffer 是一个特殊的缓存方式,我们会单独介绍。

Buffer 中有几个重要的成员属性,我们了解一下:

mark 属性我们已经不陌生了,用于重复读。capacity 描述缓存区容量,即整个缓存区最大能存储多少数据量。address 用于操作直接内存,区别于 jvm 内存,这一点待会说明。

而 position 和 limit 我想用一张图结合解释:

由于缓存区是读写共存的,所以不同的模式下,这两个变量的值也具有不同的意义。

写模式下,所谓写模式就是将缓存区中的内容写入通道。position 代表下一个字节应该被写出去的字节在缓存区中的位置,limit 表示最后一个待写字节在缓存区的位置。

读模式下,所谓读模式就是从通道读取数据到缓存区。position 代表下一个读出来的字节应当存储在缓存区的位置,limit 等于 capacity。

相关的读写操作细节,待会会和大家一起看源码,以加深对通道和缓存区协作工作的原理,这里我们先讨论一个大家可能没怎么关注过的一个问题。

JVM 内存划分为栈和堆,这是大家深入脑海的知识,但是其实划分给 JVM 的还有一块堆外内存,也就是直接内存,很多人不知道这块内存是干什么用的。

这是一块物理内存,专门用于 JVM 和 IO 设备打交道,Java 底层使用 C 语言的 API 调用操作系统与 IO 设备进行交互。

例如,Java 内存中有一个字节数组,现在调用流将它写入磁盘文件,那么 JVM 首先会将这个字节数组先拷贝一份到堆外内存中,然后调用 C 语言 API 指明将某个连续地址范围的数据写入磁盘。

读操作也是类似,而 JVM 额外做的拷贝工作也是有意义的,因为 JVM 是基于自动垃圾回收机制运行的,所有内存中的数据会在 GC 时不停的被移动,如果你调用系统 API 告诉操作系统将内存某某位置的内存写入磁盘,而此时发生 GC 移动了该部分数据,GC 结束后操作系统是不是就写错数据了。

所以,JVM 对于与外围 IO 设备交互的情况下,都会将内存数据复制一份到堆外内存中,然后调用系统 API 间接的写入磁盘,读也是类似的。由于堆外内存不受 GC 管理,所以用完一定得记得释放。

理解这一个小知识是看懂源码实现的前提,不然你可能不知道代码实现者在做什么。好了,那我们就先来看看读操作的基本使用与源码实现。

我们看这么一段代码,这段代码我大致分成了四个部分,第一部分用于获取文件通道,第二部分用于分配缓存区并完成读操作,第三部分用于将缓存区中数据进行打印,第四部分为关闭通道连接。

第一部分:

getChannel 方法用于获取一个文件相关的通道实例,具体实现如下:

getChannel 方法会调用 FileChannelImpl 的工厂方法构建一个 FileChannelImpl 实例,FileChannelImpl 是抽象类 FileChannel 的一个子类实现。

构成 FileChannelImpl 实例所需的必要参数有,该文件的文件指针,该文件的完整路径,读写权限等。

第二部分:

Buffer 的基本结构我们上述已经简单介绍了,这里不再赘述了,所谓的缓存区,本质上就是字节数组。

ByteBuffer 实例的构建是通过工厂模式产生的,必须指定参数 capacity 作为内部字节数组的容量。HeapByteBuffer 是虚拟机的堆上内存,所有数据都将存储在堆空间,我们不久将会介绍它的一个兄弟,DirectByteBuffer,它被分配在堆外内存中,具体的一会说。

这个 HeapByteBuffer 的构造情况我们不妨跟进去看看:

调用父类的构造方法,初始化我们在 ByteBuffer 中提过的一些属性值,如 position,capacity,mark,limit,offset 以及字节数组 hb。

接着,我们看看这个 read 方法的调用链。

这个 read 方法是子类 FileChannelImpl 对父类 FileChannel read 方法的重写。这个方法不是读操作的核心,我们简单概括一下,该方法首先会拿到当前通道实例的锁,如果没有被其他线程占有,那么占有该锁,并调用 IOUtil 的 read 方法。

IOUtil 的 read 方法内部也调用了很多方法,有的甚至是本地方法,这里只简单介绍一下整个 read 方法的大体逻辑,具体细节留待大家自行学习。

首先判断我们的 ByteBuffer 实例是不是一个 DirectBuffer,也就是判断当前的 ByteBuffer 实例是不是被分配在直接内存中,如果是,那么将调用readIntoNativeBuffer 方法从磁盘读取数据直接放入 ByteBuffer 实例所在的直接内存中。

否则,虚拟机将在直接内存区域分配一块内存,该内存区域的首地址存储在 var5 实例的 address 属性中。

接着从磁盘读取数据放入 var5 所代表的直接内存区域中。

最后,put 方法会将 var5 所代表的直接内存区域中的数据写入到 var1 所代表的堆内缓存区并释放临时创建的直接内存空间。

这样,我们传入的缓存区中就成功的被读入了数据。写操作是相反的,大家可以自行类比,反正堆内数据想要到达磁盘就必定要经过堆外内存的复制过程。

第三第四部分比较简单,这里不再赘述了。提醒一下,想要更好的使用这个通道和缓存区进行文件读写操作,你就一定得对缓存区的几个变量的值时刻把握住,position 和 limit 当前的值是什么,大致什么位置,一定得清晰,否则这个读写共存的缓存区可能会让你晕头转向。

选择器 Selector

Selector 是 Java NIO 的一个组件,它用于监听多个 Channel 的各种状态,用于管理多个 Channel。但本质上由于 FileChannel 不支持注册选择器,所以 Selector 一般被认为是服务于网络套接字通道的。

而大家口中的「NIO 是非阻塞的」,准确来说,指的是网络编程中客户端与服务端连接交换数据的过程是非阻塞的。普通的文件读写依然是阻塞的,和 IO 是一样的,这一点可能很多初学者会懵,包括我当时也总想不通为什么说 NIO 的文件读写是非阻塞的,明明就是阻塞的。

创建一个选择器一般是通过 Selector 的工厂方法,Selector.open :

而一个通道想要注册到某个选择器中,必须调整模式为非阻塞模式,例如:

以上代码是注册一个通道到选择器中的最简单版本,支持注册选择器的通道都有一个 register 方法,该方法就是用于注册当前实例通道到指定选择器的。

该方法的第一个参数就是目标选择器,第二个参数其实是一个二进制掩码,它指明当前选择器感兴趣当前通道的哪些事件。以枚举类型提供了以下几种取值:

int OP_READ = 1 << 0;

int OP_WRITE = 1 << 2;

int OP_CONNECT = 1 << 3;

int OP_ACCEPT = 1 << 4;

这种用二进制掩码来表示某些状态的机制,我们在讲述虚拟机类类文件结构的时候也遇到过,它就是用一个二进制位来描述一种状态。

register 方法会返回一个 SelectionKey 实例,该实例代表的就是选择器与通道的一个关联关系。你可以调用它的 selector 方法返回当前相关联的选择器实例,也可以调用它的 channel 方法返回当前关联关系中的通道实例。

除此之外,SelectionKey 的 readyOps 方法将返回当前选择感兴趣当前通道中事件中准备就绪的事件集合,依然返回的一个整型数值,也就是一个二进制掩码。

例如:

假如 readySet 的值为 13,二进制 「0000 1101」,从后向前数,第一位为 1,第三位为 1,第四位为 1,那么说明选择器关联的通道,读就绪、写就绪,连接就绪。

所以,当我们注册一个通道到选择器之后,就可以通过返回的 SelectionKey 实例监听该通道的各种事件。

当然,一旦某个选择器中注册了多个通道,我们不可能一个一个的记录它们注册时返回的 SelectionKey 实例来监听通道事件,选择器应当有方法返回所有注册成功的通道相关的 SelectionKey 实例。

selectedKeys 方法会返回选择器中注册成功的所有通道的 SelectionKey 实例集合。我们通过这个集合的 SelectionKey 实例,可以得到所有通道的事件就绪情况并进行相应的处理操作。

下面我们以一个简单的客户端服务端连接通讯的实例应用一下上述理论知识:

服务端代码:

这段小程序的运行的实际效果是这样的,客户端建立请求到服务端,待请求完全建立,客户端会去检查服务端是否有数据写回,而服务端的任务就很简单了,接受任意客户端的请求连接并为它写回一段数据。

别看整个过程很简单,但只要你有一点模糊的地方,你这个功能就不可能实现,不信你试试,尤其是加了选择器的客户端代码,更值得大家一行一行分析。提醒一点的是,大家应更多的关注于哪些方法是阻塞的,哪些是非阻塞的,这会有助于分析代码。

这其实也算一个最最简单的服务器客户端请求模型了,理解了这一点相信会有助于理解浏览器与 Web 服务器的工作原理的,这里我就不再带大家分析了,有任何不同看法的也欢迎给我留言,咱们一起学习探讨。

想必你也能发现,加了选择器的代码会复杂很多,也并不一定高效于原来的代码,这其实是因为你的功能比较简单,并不涉及大量通道处理,逻辑一旦复杂起来,选择器给你带来的好处会非常明显。

其实,NIO 中还有一块 AIO ,也就是异步 IO 并没有介绍,因为异步 IO 涉及到很多其他方面知识,这里暂时不做介绍,后续文章将单独介绍异步任务等相关内容。

 

注:关注作者微信公众号,了解更多分布式架构、微服务、netty、MySQL、spring、、性能优化、等知识点。公众号:《Java烂猪皮》

 

© 著作权归作者所有

共有 人打赏支持
OSC_烂猪皮
粉丝 21
博文 15
码字总数 44171
作品 0
黄浦
加载中

评论(15)

OSC_烂猪皮
OSC_烂猪皮

引用来自“广州首席架构师_0”的评论

挺清晰
我总感觉还差点什么
OSC_烂猪皮
OSC_烂猪皮

引用来自“爱吃大肉包”的评论

读操作也是类似,而 JVM 额外做的拷贝工作也是有意义的,因为 JVM 是基于自动垃圾回收机制运行的,所有内存中的数据会在 GC 时不停的被移动,如果你调用系统 API 告诉操作系统将内存某某位置的内存写入磁盘,而此时发生 GC 移动了该部分数据,GC 结束后操作系统是不是就写错数据了。

--这里是在描述用户态到内核态的一个内存转换么?··感觉有点问题,意识是不用GC就不用copy到内核内存了么
我也觉得有点问题,所以请各位大佬们指点指点,请教一下
OSC_烂猪皮
OSC_烂猪皮

引用来自“whaon”的评论

基本用bio或者netty之类,nio真的有人用?
还是会有人用的
爱吃大肉包
爱吃大肉包
读操作也是类似,而 JVM 额外做的拷贝工作也是有意义的,因为 JVM 是基于自动垃圾回收机制运行的,所有内存中的数据会在 GC 时不停的被移动,如果你调用系统 API 告诉操作系统将内存某某位置的内存写入磁盘,而此时发生 GC 移动了该部分数据,GC 结束后操作系统是不是就写错数据了。

--这里是在描述用户态到内核态的一个内存转换么?··感觉有点问题,意识是不用GC就不用copy到内核内存了么
whaon
whaon
基本用bio或者netty之类,nio真的有人用?
黑牙菜菜
黑牙菜菜
没看明白的以后都别去面试了
黑牙菜菜
黑牙菜菜
....都讲烂了。。
OSC_烂猪皮
OSC_烂猪皮

引用来自“依旧灯火阑珊”的评论

楼主辛苦了,但是,这个,深入理解,是不是不合适:sweat:。或者出个系列,然后这篇文章名字为深入理解:XXX系列(一)
这个可以有
依旧灯火阑珊
依旧灯火阑珊
楼主辛苦了,但是,这个,深入理解,是不是不合适:sweat:。或者出个系列,然后这篇文章名字为深入理解:XXX系列(一)
zjg23
zjg23
服务端代码没显示出来
Java NIO ByteBuffer学习

深入理解Apache Mina (6)---- Java Nio ByteBuffer与Mina ByteBuffer的区别 http://chinaestone.iteye.com/blog/468138 DirectBuffer及内存泄漏 http://blog.csdn.net/zhouhl_cn/article/d......

mj4738
2012/10/15
0
0
一线互联网公司研发工程师(Java)月薪25-50k的技能要求整理

良好的开发基础,精通Java语言基础,熟练掌握多线程开发技术, 对于Java基础技术体系(包括JVM、类装载机制、JUC、NIO、网络)有深入的理解和实践经验; 熟练掌握常用Java技术框架,能编写高...

vvx1024
01/06
0
0
Java NIO原理 图文分析及代码实现

Java NIO原理图文分析及代码实现 前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术...

囚兔
2015/04/29
0
0
Java NIO原理图文分析及代码实现

前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。可以参考:http://baik...

SunnyWu
2014/11/05
0
1
java NIO原理及通信模型

Java NIO是在jdk1.4开始使用的,它既可以说成“新IO”,也可以说成非阻塞式I/O。下面是java NIO的工作原理: 由一个专门的线程来处理所有的IO事件,并负责分发。 事件驱动机制:事件到的时候...

柳哥
2015/02/15
0
4
mina 之 java.lang.OutOfMemoryError

前段时间在测试过程中发现了mina框架的问题:当mina一次传输的文件超过一定值(如55m)或者连续传输文件的次数过于频繁,就会内存溢出: org.apache.mina.filter.codec.ProtocolEncoderExce...

班帅
2014/09/02
0
0
我所理解的JVM(一):内存结构

Java Memory Model 简称JMM,译为Java内存模型,属于Java虚拟机规范的一部分 java虚拟机运行时的内存区域分为:程序计数器、虚机机栈、本地方法栈、堆、方法区5部分。其中前3部分是线程独享的...

康斯但丁
2017/10/28
0
0
面试中关于Java虚拟机(jvm)的问题看这篇就够了

最近看书的过程中整理了一些面试题,面试题以及答案都在我的文章中有所提到,希望你能在以问题为导向的过程中掌握虚拟机的核心知识。面试毕竟是面试,核心知识我们还是要掌握的,加油~~~ 下面...

snailclimb
05/12
0
0
Java 使用 happen-before 规则实现共享变量的同步操作

前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。按照官方的...

stateIs0
01/20
0
0
Java NIO原理图文分析及代码实现

Java IO 在Client/Server模型中,Server往往需要同时处理大量来自Client的访问请求,因此Server端需采用支持高并发访问的架构。一种简单而又直接的解决方案是“one-thread-per-connection”。...

只想一个人静一静
2014/02/22
0
1

没有更多内容

加载失败,请刷新页面

加载更多

下一页

about git flow

  昨天元芳做了git分支管理规范的分享,为了拓展大家关于git分支的认知,这里我特意再分享这两个关于git flow的链接,大家可以看一下。 Git 工作流程 Git分支管理策略   git flow本质上是...

qwfys
今天
2
0
Linux系统日志文件

/var/log/messages linux系统总日志 /etc/logrotate.conf 日志切割配置文件 参考https://my.oschina.net/u/2000675/blog/908189 dmesg命令 dmesg’命令显示linux内核的环形缓冲区信息,我们可...

chencheng-linux
今天
1
0
MacOS下给树莓派安装Raspbian系统

下载镜像 前往 树莓派官网 下载镜像。 点击 最新版Raspbian 下载最新版镜像。 下载后请,通过 访达 双击解压,或通过 unzip 命令解压。 检查下载的文件 ls -lh -rw-r--r-- 1 dingdayu s...

dingdayu
今天
1
0
spring boot使用通用mapper(tk.mapper) ,id自增和回显等问题

最近项目使用到tk.mapper设置id自增,数据库是mysql。在使用通用mapper主键生成过程中有一些问题,在总结一下。 1、UUID生成方式-字符串主键 在主键上增加注解 @Id @GeneratedValue...

北岩
今天
2
0
告警系统邮件引擎、运行告警系统

告警系统邮件引擎 cd mail vim mail.py #!/usr/bin/env python#-*- coding: UTF-8 -*-import os,sysreload(sys)sys.setdefaultencoding('utf8')import getoptimport smtplibfr......

Zhouliang6
今天
1
0
Java工具类—随机数

Java中常用的生成随机数有Math.random()方法及java.util.Random类.但他们生成的随机数都是伪随机的. Math.radom()方法 在jdk1.8的Math类中可以看到,Math.random()方法实际上就是调用Random类...

PrivateO2
今天
3
0
关于java内存模型、并发编程的好文

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在...

DannyCoder
昨天
1
0
dubbo @Reference retries 重试次数 一个坑

在代码一中设置 成retries=0,也就是调用超时不用重试,结果DEBUG的时候总是重试,不是0吗,0就不用重试啊。为什么还是调用了多次呢? 结果在网上看到 这篇文章才明白 https://www.cnblogs....

奋斗的小牛
昨天
2
0
数据结构与算法3

要抓紧喽~~~~~~~放羊的孩纸回来喽 LowArray类和LowArrayApp类 程序将一个普通的Java数组封装在LowArray类中。类中的数组隐藏了起来,它是私有的,所以只有类自己的方法才能访问他。 LowArray...

沉迷于编程的小菜菜
昨天
1
0
spring boot应用测试框架介绍

一、spring boot应用测试存在的问题 官方提供的测试框架spring-boot-test-starter,虽然提供了很多功能(junit、spring test、assertj、hamcrest、mockito、jsonassert、jsonpath),但是在数...

yangjianzhou
昨天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部