刚刚开始工作的时候,就去看了有关的nio
的书,但是并没有真正理解其原理,所以看完忘,忘完看。最近又去看,计划着将这次理解的东西记录下来。
我认为在学习之前需要了解很多东西,在理解了这些东西后,无论是在网上看别人写的nio
资料,还是自己去看有关的书,就不会感觉到这里不知道,那里不明白。
我在学习总结的过程中,因为这些东西涉及面比较广,一个一个去学习原理会相当慢,所以大部分的资料来源都是网上抄来的,其中有的资料已经很久远了,可能与现在的技术并不吻合,又由于自己水平有限,很多的知识可能理解本来就是有误的,所以要去了解原理,请查看权威素材。还有一些东西是自己也没有搞懂的或者是没有注意到的,所以不对的地方希望大家多多指正。
我主要认为可以从以下方面来讲解:
用户空间与内核空间
IO系统简介:轮询、中断、DMA、通道
直接内存映射
虚拟内存
操作系统页
同步异步、阻塞非阻塞
BIO与NIO区别,AIO
NIO主要内容
其他常见概念
用户空间与内核空间
计算机启动时,将物理内存划分为了系统空间与用户空间,这是逻辑上的划分。随后将操作系统的数据存放在系统空间中,而用户进程的数据则是存在于用户空间。JVM
是常规进程,驻守在用户空间,用户空间是非特权区域,该区域执行的代码不能直接访问硬件设备。内核空间是操作系统所在的区域,他能与设备控制器通讯。
也就是说JVM
中运行的java
程序想要与设备、硬盘等进行交换数据需要通过内核空间交换,先拿使用IO
流读取磁盘文件作为例子来说明用户空间读取设备信息的过程。
java
程序中的调用read
函数,最终调用本地native
方法,此时该方法去内核空间取数据,若内核缓冲区有需要的数据;则返回,若没有则等待数据到来。- 处理器将磁盘中的文件读入内核空间中,这个拷贝过程可以由处理器完成,当前操作系统由
DMA
方式来完成。
IO系统简介:轮询、中断、DMA、通道
转载地址:
https://www.zhihu.com/question/31274481
https://blog.csdn.net/ajian005/article/details/18182953
https://blog.csdn.net/dark_tone/article/details/52617184
IO
数据传输的四种方式分为:轮训方式、程序中断、DMA方式与通道方式。
轮询方式
计算机的IO
测试指令通过轮询的方式,检测IO
设备的忙/闲标志,决定主存和外设之间是或否传出一个字或者一个字符。在这种情况下,CPU
的大量时间在等待输入、输出的循环检测上,使计算机不能充分发挥效率,外设也得不到合理的使用,整个系统效率低下。程序轮询毕竟占据了CPU
相当一部分处理时间,因此程序轮询是一种效率较低的方式,在现代计算机系统中已很少应用。
程序中断
中断的方式下,CPU
与IO
设备之间的传输数据步骤如下:
- 在某个进程需要数据时,发出指令启动输入输出设备准备数据
- 进程发出指令启动设备之后,该进程放弃处理器,此时,进程调度程序会调度其他就绪进程使用处理器。然后
IO
设备的控制器逐个比特的从设备中读取一块数据放入设备的内部缓冲区中,并计算该块数据的校验和,以保证读取的正确性。校验完成后,此IO
操作相当于完成 - 当
IO
操作完成时,输入输出设备控制器通过中断请求线向处理器发出中断信号,处理器收到中断信号之后,转向预先设计好的中断处理程序,操作系统开始逐个字节地从缓冲区中数据读入内存,然后对数据传送工作进行相应的处理。 - 得到了数据的进程,转入就绪状态。在随后的某个时刻,进程调度程序会选中该进程继续工作。
中断方式的优点
- 设备控制器发出中断信号中断机制的引入,使得外围设备有了反映自身状态的能力,仅当
IO
操作正常或者异常结束时才中断CPU
,从而实现了一定程度的并行。
中断方式的缺点
- 首先,现代计算机系统通常配置有各种各样的输入输出设备。如果这些
IO
设备都同过中断处理方式进行并行操作,那么中断次数的急剧增加会造成CPU
无法响应中断和出现数据丢失现象。 - 其次,如果
IO
控制器的数据缓冲区比较小,在缓冲区装满数据之后将会发生中断。那么,在数据传送过程中,发生中断的机会较多,这将耗去大量的CPU
处理时间。
DMA方式(直接存储器访问)
直接内存存取技术是指,数据在内存与IO
设备间直接进行成块传输。DMA
有两个技术特征,首先是直接传送,其次是块传送。 直接传送,即在内存与IO
设备间传送一个数据块的过程中,不需要CPU
的任何中间干涉,只需要CPU
在过程开始时向设备发出“传送块数据”的命令,然后通过中断来得知过程是否结束和下次操作是否准备就绪。
DMA工作过程
- 当进程要求设备输入数据时,
CPU
把准备存放输入数据的内存起始地址以及要传送的字节数分别送入DMA
控制器中的内存地址寄存器和传送字节计数器。 - 发出数据传输要求的进行进入等待状态。此时正在执行的
CPU
指令被暂时挂起。进程调度程序调度其他进程占据CPU
。 - 输入设备不断地窃取
CPU
工作周期(又叫周期挪用),将数据缓冲寄存器中的数据源源不断地写入内存,直到所要求的字节全部传送完毕。 DMA
控制器在传送完所有字节时,通过中断请求线发出中断信号。CPU
在接收到中断信号后,转入中断处理程序进行后续处理。- 中断处理结束后,
CPU
返回到被中断的进程中,或切换到新的进程上下文环境中,继续执行。
DMA方式的优缺点
DMA
窃取CPU
时钟周期,CPU
处理效率降低了,要想尽量少地窃取始终周期,就要设法提高DMA
控制器的性能,这样可以较少地影响CPU
出理效率。
通道方式(进化版的DMA)
通道相当于更加进化版的dma控制器,目的在于让IO
自己完成输入输出,指令使用通道指令。CPU
的IO
指令仅仅发了一个命令信号,而实际工作是由通道(微处理器)自己的程序来完成的。 而传统的DMA
需要CPU
指令指导。
输入/输出通道是一个独立于CPU
的,专门管理IO
的处理机,它控制设备与内存直接进行数据交换。它有自己的通道指令,这些通道指令由CPU
启动,并在操作结束时向CPU
发出中断信号。输入/输出通道控制是一种以内存为中心,实现设备和内参内直接交换数据的控制方式。在通道方式中,数据的传送方向、存放数据的内存起始地址以及传送的数据块长度等都由通道来进行控制。另外,通道控制方式可以做到一个通道控制多台设备与内存进行数据交换。因而,通道方式进一步减轻了CPU
的工作负担,增加了计算机系统的并行工作程度。
输入/输出通道分类
按照信息交换方式和所连接的设备种类不同,通道可以分为以下三种类型:
- 字节多路通道 :它适用于连接打印机、终端等低速或中速的
IO
设备。这种通道以字节为单位交叉工作:当为一台设备传送一个字节后,立即转去为另一它设备传送一个字节。 - 选择通道:它适用于连接磁盘、磁带等高速设备。这种通道以“组方式”工作,每次传送一批数据,传送速率很高,但在一段时间只能为一台设备服务。每当一个
IO
请求处理完之后,就选择另一台设备并为其服务。 - 成组多路通道:这种通道综合了字节多路通道分时工作和选择通道传输速率高的特点,其实质是:对通道程序采用多道程序设计技术,使得与通道连接的设备可以并行工作。
四种方式区别
DMA与中断的区别
- 中断方式是在数据缓冲寄存器满之后发出中断,要求CPU进行中断处理,而DMA方式则是在所要求传送的数据块全部传送结束时要求
CPU
进行中断处理。这就大大减少了CPU
进行中断处理的次数。 - 中断方式的数据传送是在中断处理时由CPU控制完成的,而DMA方式则是在DMA控制器的控制下,不经过
CPU
控制完成的。这就排除了CPU
因并行设备过多而来不及处理以及因速度不匹配而造成数据丢失等现象。
CPU介入:
- 轮询方式:完全介入
- 程序中断:需要
CPU
介入,但在数据读入IO
设备的缓冲区,发出中断前,CPU
可以做其他事务 - DMA:在传输开始(
DMA
初始化)和传输结束(中断)时介入
硬件接口支持
- 循环
IO
测试:最简单的硬件 - 程序中断:增加中断控制器
- DMA:增加
DMA
直接内存映射
当使用DMA
方式时,并没有改变用户空间,内核空间,磁盘间的数据交互,这个过程简化了底层操作,但是对于用户空间中的java程序
来说,并没有上面变化。
从上面的图中可以看出,这个过程有点冗余,需要对数据进行两次拷贝,磁盘拷贝入内核空间,内核空间拷贝到用户空间。于是就出现了直接内存映射。java
程序申请一块不属于用户空间,页也不属于内核空间的物理内存,当使用时会将磁盘文件拷贝到这块内存中,用户空间直接读取这块内存中数据,减少了一次拷贝。
- ①表示
java
程序申请一块直接内存(物理内存块) - ②③表示
java
程序执行系统调用,要求将数据填充到物理内存中 - ④表示数据拷贝到了物理内存块中
- ⑤表示
java
程序直接操作物理内存空间中的数据
虚拟内存
主要来自https://www.cnblogs.com/qionglouyuyu/p/4175484.html
一个进程是与其他进程共享CPU
与内存的。当多个进程启动后,就有可能出现下面问题:
- 其中一个进程
A
占用了大部分的内存,此时如果进程B
也需要很大的内存空间,现在只能将A
在内存中的内容先存到磁盘中 某个进程的寻址,为了避免与其他进程冲突,就需要使用逻辑地址
为了应对上面的问题,就出现了虚拟内存,我们先为每个进程分配一部分的内存,然后将磁盘中的一部分存储空间与分配的那部分空间组合成一个逻辑块分配给进程。此时每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为页(
Page
),每个页都是一段连续的地址(下图将这个进程的地址空间分为5个页)。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上。如下图所示:
虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。
操作系统页
在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address
)映射到存储器上的物理地址。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表好比是目录,。
缺页中断:指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。通常情况下,用于处理此中断的程序是操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。
同步异步、阻塞非阻塞
这是一个我理解了一遍又一遍的东西,到现在仍然有点懵,不过看过了很多网上的博客资料,主要参考的是这些博客:
知乎上有关这个问题的解答:https://www.zhihu.com/question/19732473/answer/20851256
线程中的同步异步、阻塞非阻塞以及IO的具体情况:http://www.cnblogs.com/dolphin0520/p/3916526.html
IO中的同步异步、阻塞非阻塞:https://www.cnblogs.com/zhuYears/archive/2012/09/28/2690194.html
《UNIX网络编程》 中5种IO模型:https://blog.csdn.net/tjiyu/article/details/52959418
下面是我对他们的理解
IO的同步异步、阻塞非阻塞
通常来说,IO
操作包括:磁盘IO
,socketIO
,外设IO
。
当一个IO
操作读发生时,有两个步骤
- 等待数据准备
- 将数据从内核空间拷贝到用户空间
比如对于socket
来说,步骤一等待数据包到达,到达后拷贝到内核空间缓冲区,步骤二将内核缓冲区数据拷贝到用户空间缓冲区中
阻塞与非阻塞
IO
的阻塞、非阻塞主要表现在一个IO
操作过程中,如果有些操作很慢,比如读操作时需要准备数据,那么当前IO进程是否等待操作完成,还是得知暂时不能操作后先去做别的事情?一直等待下去,什么事也不做直到完成,这就是阻塞。抽空做些别的事情,这是非阻塞。
非阻塞IO
会在发出IO
请求后立即得到回应,即使数据包没有准备好,也会返回一个错误标识,使得操作进程不会阻塞在那里。操作进程会通过多次请求的方式直到数据准备好,返回成功的标识。
从CPU
角度可以看出非阻塞明显提高了CPU
的利用率,进程不会一直在那等待。但是同样也带来了线程切换的增加。增加的 CPU
使用时间能不能补偿系统的切换成本需要好好评估。
UNIX网络编程中的同步异步、阻塞非阻塞
这是好多地方引用的两句话:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;(同步IO会造成请求的线程阻塞,请求线程会一直等待返回所需的数据)
An asynchronous I/O operation does not cause the requesting process to be blocked; (异步IO不会造成请求的线程被阻塞)
《UNIX网络编程》中有5种IO模型,前4种为同步IO操作,只有异步IO模型是异步IO操作
- 阻塞IO模型
- 非阻塞IO模型
- IO复用模型
- 信号驱动的IO模型
- 异步IO模型
阻塞IO模型
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。应用场景为:阻塞Socket
、BIO
非阻塞IO模型
进程发起IO
系统调用后,如果内核缓冲区没有数据,需要到IO
设备中读取,进程返回一个错误而不会被阻塞;进程发起IO
系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。应用场景为:socket
是非阻塞的方式(设置为NONBLOCK
)
对于阻塞IO
模型来说, 内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
IO复用模型
多个的进程的IO可以注册到一个复用器(select
)上,然后用一个进程调用该select
, select
会监听所有注册进来的IO
; 如果select
没有监听的IO
在内核缓冲区都没有可读数据,select
调用进程会被阻塞;而当任一IO
在内核缓冲区中有可数据时,select
调用就会返回;而后select
调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO
,读取内核中准备好的数据。 多个进程注册IO
后,只有另一个select
调用进程被阻塞。
典型应用:select
、poll
、epoll
三种方案,nginx
都可以选择使用这三个方案;Java NIO
。
信号驱动IO模型
当进程发起一个IO
操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO
读取数据。回调机制,实现、开发应用难度大;
异步IO模型
当进程发起一个IO
操作,进程返回(不阻塞),但也不能返回结果;内核把整个IO
处理完后,会通知进程结果。如果IO
操作成功则进程直接获取到数据。典型应用:JAVA7 AIO
、高性能服务器应用。
5 种IO模型比较
硬盘IO
硬盘IO
就是文件IO
,对于文件的判断数据是否准备好来说,都是阻塞的。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 IO
操作延迟很少。所以数据的准备好与否,对文件IO
影响不大。
对于文件IO
来说,重要的是异步IO
,它允许一个进程可以从操作系统请求一个或多个 IO
操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的 IO
操作已完成的通知。
异步文件IO使用的是AsynchronousFileChannel
,这个类的使用在nio通道篇介绍
应用与应用之间的关系
node.js是异步非阻塞编程。ajax
同步、异步调用。WebService
的同步异步调用。以A
应用调用B
应用为例:
同步与异步
同步与异步关注的是消息通信机制。假如A
去调用B
,那么就是指的是A
调用B
的结果是怎么获得的。
- [ ] 同步是指
A
发出对B
的调用,A
一直等待在返回结果,B
返回结果后,这个调用结束。 - [ ] 异步是指
A
发出对B
的调用,此时不会返回最终的结果,A
需要的结果会由B
计算或执行后回调A
或是通过状态通知来通知A
。
阻塞与非阻塞
阻塞与非阻塞关注的是程序在等待调用结果的时的状态。还是以A
调用B
来说,指的是A
调用B
后,A
的状态。
- [ ] 阻塞是指
A
调用B
后,A
被挂起,无法做其他事情,只是在等待B
有结果时,才结束继续执行。 - [ ] 非阻塞是指
A
调用B
后,A
没有得到结果前,A
可以去做其他事情,只是A
需要结果时,A
就去向B
查询刚才的调用B
是否已经结束。
CPU上的同步异步、阻塞非阻塞
在CPU
层次,或者说操作系统进行IO
和任务调度的层次,现代操作系统通常使用异步非阻塞方式进行IO
(有少部分IO
可能会使用同步非阻塞轮询),即发出IO
请求之后,并不等待IO
操作完成,而是继续执行下面的指令(非阻塞),IO
操作和CPU
指令互不干扰(异步),最后通过中断的方式来通知IO
操作完成结果。
线程上的同步异步、阻塞非阻塞
这两者不是在一个维度上的东西,同步异步更多表示的是一个任务的执行是否对其他任务有影响。阻塞非阻塞强调的则是发起调用后对自己的影响。
线程上的同步异步,更多表示的是多个任务要同时发生,多个任务之间的关系。
- [ ] 同步是:其他任务需要等待某个任务发生完后再去执行,比如加了
synchronized
的代码,多个线程要执行含有synchronized
的代码块,就需要等待,这就是同步 [ ] 异步是:这些任务可以并发的执行,一个任务的执行不影响其他任务的执行
[ ] 线程上的阻塞非阻塞是指发出调用后,自己处于哪种状态。
[ ] 阻塞是:任务执行过程中发出请求后,由于该请求的操作需要的条件不足,那么该任务会一致等待,直到条件满足。
非阻塞: 发出请求后,如果条件不满足,则直接返回条件不满足,不会一直在等待。当需要结果时,再去查看结果是否产生。
那么java
多线程中的Future
是异步还是非阻塞?FutureTask
提交后,当前线程继续执行,提交的任务也继续执行,当提交的任务执行完成后,将任务的state
设置为其他值,这个相当于回调,这是异步。程序运行到这一步,主要流程已经执行完了,但是当主线程调用get()
或isDone()
时候,又相当于是非阻塞。这个我感觉并不冲突,多线程间的异步同步表示的两个线程间的协作关系,而阻塞非阻塞表示的是当前线程所处的状态。
BIO与NIO区别,AIO
bio
是面向流的,nio
是面向块的。JVM
运行字节码的速率已经接近于本地编译代码,也就是说多数java
程序已不再受CPU
的束缚,而更多是受IO
的束缚。操作系统并非不能快速的传输数据,让Java
有事可做,而是JVM
本身在IO
方面效率欠佳。操作系统与java
基于流的模式不匹配,操作系统要移动的是大块数据(缓冲区),这往往是硬件直接存储器存储(DMA)的协助下完成。而JVM
的IO
更喜欢操作小块数据,单个字节,几行文本结果,操作系统送来整缓冲区的数据,java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。所以在一些情况下nio
效率更高。nio
使用Channel
通道来传输数据,通道是可以是双向的,而流只能是在一个方向上移动。由于通道是双向的,所以更能反映底层操作系统的真实情况。nio
中有的有的操作时非阻塞的,而io
中的各种流是阻塞的。此处的阻塞是什么?内存可以分为用户空间与系统空间。
JVM
属于用户空间,操作系统属于系统空间。用户空间无法直接与设备(比如硬盘、鼠标)进行数据交互。此时①若用户进程需要读取磁盘上的数据时,首先调用一个系统指令,表示我要获得一些数据,
此时②操作系统与硬件进行交互将数据从硬件读入系统空间,
然后③将系统空间中的数据放入用户空间制定的位置。
问题出在第②步,如果这个过程由
CPU
直接操作,并且立即就读取到了,那么也谈不上阻塞。但是,CPU将数据由设备拷入内核空间这一步操作的时候,硬件上的数据并没有准备好,此时CPU
可能就去搞其他事情了,那么现在的read()
就会一直等待数据读到内核空间,也就是当前线程就会阻塞。
选择器:如果没有选择器,那么就需要开启第一个线程,在每个线程上进行阻塞读或者写。有了选择器,此时可以在一个线程上打开多个通道,然后将通道注册到选择器上,利用选择器监听通道上感兴趣的事件。
下面内容来自:https://www.cnblogs.com/xiexj/p/6874654.html
- [ ] BIO:同步阻塞式
IO
,服务器端与客户端三次握手简历连接后一个链路建立一个线程进行面向流的通信。这曾是jdk1.4
前的唯一选择。在任何一端出现网络性能问题时都影响另一端,无法满足高并发高性能的需求。 [ ] NIO:同步非阻塞
IO
,以块的方式处理数据。采用多路复用Reactor
模式。JDK1.4
时引入。[ ] AIO:异步非阻塞
IO
,基于unix
事件驱动,不需要多路复用器对注册通道进行轮询,采用Proactor
设计模式。JDK1.7
时引入。
NIO的主要内容
nio
中主要增加的内容如下:
- 缓冲区(
Buffers
) - 通道(
Channels
) - 文件锁定与内存映射文件(
File locking and memory-mapped files
) - 套接字(
Sockets
) - 选择器(
Selectors
) - 正则表达式(
Regular expressions
) - 字符集(
Character sets
)
在nio
开头的包中,可以看到如下子包:
各个子包的主要内容:
- java.nio:主要是
Buffer
的实现类、有关Buffer
的异常、byte
到基本数据类型的转换以及大端模式与小端模式数据的转换类 - java.nio.channels:主要是异步通道、可选通道、通道异常、散列聚集通道、可中断通道以及通道的相关异常类
- java.nio.channels.spi:可选通道、可中断通道等的抽象类定义,可选通道、异步通道具体实现的产生者。
- java.nio.charset:字符集定义、转换编码类以及编码的异常类
- java.nio.charset.spi:字符集提供者
- java.nio.file:nio中添加的有关的文件操作,我认为是相当于直接与操作系统中的文件系统进行交互,所以这个包下文件主要定义的是文件系统以及文件系统中文件相关的信息抽象,甚至还可以监视文件目录或文件内容的改变等等。
- java.nio.file.attribute:有关文件系统中文件的一些属性,比如创建时间、修改时间、创建者、权限、文件所占空间等等。
- java.nio.file.spi:文件系统实现类提供者
其他常见概念
其他主要介绍:
- [x] spi
- [x] posix
- [x] 文件空洞
- [x] copy-on-write
- [x] MappedByteBuffer操作大文件原理
- [x] jvm对文件的锁定,以进程为单位
- [x] 文件未关闭检查,垃圾回收关闭文件
spi
参考spi介绍:https://www.cnblogs.com/lovesqcc/p/5229353.html
不同厂商对于一些接口的实现强烈的依赖于操作系统等,不同操作系统有不同的系统调用方法。有的应用不通过java
虚拟机,而是直接调用操作系统接口会有更好的效率,此时不能要求操作系统提供相同的方法名,此时就出现了spi
这种方式。
可是在查看了nio中的spi
的provider
的获取,发现很多知识命名为spi
包,实现却不是以标准的spi
实现。
SelectorProvider 中实现获取
首先这是SelectorProvider
中获取provider
的方式,但是这种方式与标准的spi
获取方式却不相同。
上述方法实现如下:
private static boolean loadProviderFromProperty() {
String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
if (cn == null)
return false;
try {
Class<?> c = Class.forName(cn, true,
ClassLoader.getSystemClassLoader());
provider = (SelectorProvider)c.newInstance();
return true;
} catch (ClassNotFoundException x) {
throw new ServiceConfigurationError(null, x);
} catch (IllegalAccessException x) {
throw new ServiceConfigurationError(null, x);
} catch (InstantiationException x) {
throw new ServiceConfigurationError(null, x);
} catch (SecurityException x) {
throw new ServiceConfigurationError(null, x);
}
}
private static boolean loadProviderAsService() {
ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());
Iterator<SelectorProvider> i = sl.iterator();
for (;;) {
try {
if (!i.hasNext())
return false;
provider = i.next();
return true;
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
}
posix
Portable Operating System Interface for Computing System。他是一个针对操作系统(准确地说是针对类Unix操作系统)的标准化协议。这个协议是对操作系统服务接口的标准化,从而保证了应用程序在源码层次的可移植性。如今主流的Linux
系统都做到了兼容POSIX
标准。
Linux
版本的jdk
中有关nio
的一些类可以直接调用posix
标准中的本地方法。
文件空洞
下面内容来自百度百科:https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E7%A9%BA%E6%B4%9E/6169344
在UNIX
文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为 0。
如果 offset
比文件的当前长度更大,下一个写操作就会把文件“撑大(extend
)”。这就是所谓的在文件里创造“空洞(hole
)”。没有被实际写入文件的所有字节由重复的 0 表示。空洞是否占用硬盘空间是由文件系统(file system
)决定的。
- 用
ls
查看的文件大小是将空洞算在内的。 cp
命令拷贝的文件,空洞部分不拷贝,所以生成的同样文件占用磁盘空间小- 用
read
读取空洞部分读出的数据是0,所以如果用read
和write
拷贝一个有空洞的文件,那么最终得到的文件没有了空洞,空洞部分都被0给填充了,文件占用的磁盘空间就大了。不过文件大小不变。
空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务。
copy-on-write
下面内容来自百度百科:https://baike.baidu.com/item/%E5%86%99%E5%85%A5%E6%97%B6%E5%A4%8D%E5%88%B6
写入时复制(Copy-on-write
)是一个被使用在程序设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers
)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller
)尝试修改资源时,系统才会真正复制一个副本(private copy
)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently
)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy
)被建立。
也就是说采用copy-on-write
这种方式时,假如多个线程操作一个数组,读取时操作同一个源数组,但是有一个线程要写入时,会先拷贝一个数组,写入到拷贝出来的数组中。
MappedByteBuffer操作大文件原理
这篇博客有关于MappedByteBuffer
的介绍:https://www.jianshu.com/p/f90866dcbffc
下面是我的理解:
MappedByteBuffer
其中是使用DirectByteBuffer
的,然后还会使用到虚拟内存的知识,又使用了DMA
相关的东西。
- 使用
DirectByteBuffer
,相当于跳过内核空间,此时只需要将数据从硬盘拷贝到内存中就可以了 - 现在要将文件中内容拷贝到内存中,文件太大时,一次拷贝不进去,就需要进行多次的应用程序调用本地方法。此时虚拟内存就派上了用场,它将磁盘上的文件映射为一大块内存,一次本地方法调用,然后就由操作系统接管,完成文件读取。
- 在第 2 步的过程中,由于
DMA
这种方式,使得CPU
执行较少的拷贝任务,可以去做其他事情。
jvm文件的锁定,进程
可以通过两种方式对文件加锁
RandomAccessFile
构造函数,第二个参数使用rws/rwd
,rws
读写同步,rwd
读写非同步使用
channel的lock()
与tryLock()
文件锁可以分为共享锁与独占锁。此外,这两种锁还需要操作系统的支持。
共享锁:同时只能最多有一个写,可以有多个读
独占锁:读写不能同时进行
当前的所说的锁是加在文件上的,是进程锁,而不是线程锁。加的锁在调用FileLock.release()
或者Channel.close()
或者JVM
关闭时失效。
周期挪用
来自百度百科:https://baike.baidu.com/item/周期挪用/6228528?fr=aladdin
周期挪用是指利用CPU
不访问存储器的那些周期来实现DMA
操作,此时DMA
可以使用总线而不用通知CPU
也不会妨碍CPU
的工作。周期挪用并不减慢CPU
的操作,但可能需要复杂的时序电路,而且数据传送过程是不连续的和不规则的。
I/O设备要求DMA传送会遇到三种情况
- 一种是此时
CPU
不需访问主存(如CPU
正在执行乘法指令,由于乘法指令执行时间较长,此时CPU
不需访问主存),故IO
设备访存与CPU
不发生冲突。 - 第二种情况是
IO
设备要求DMA
传送时,CPU
正在访存,此时必须待存取周期结束时刻,CPU
才能将总线占有权让出。 - 第三种情况是
IO
设备要求访存时,CPU
也要求访存,这就出现了访存冲突。此刻,IO
访存优先于CPU
访存,因为IO
不立即访存就可能丢失数据,这时IO
要窃取一二个存取周期,意味着CPU
在执行访存指令过程中插入了DMA
请求,并挪用了一二个存取周期,使CPU
延缓了一二个存取周期再访存。