知识点
- 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
- 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理
1. io流的分类
字节流和字符流的分类
字节输入流:InputStream,字节输出流:OutputStream
字符输入流:Reader,字符输出流:Writer
什么是字节流?
字节流–传输过程中,传输数据的最基本单位是字节的流。
什么是字符流?
字符流–传输过程中,传输数据的最基本单位是字符的流。
字符流的缓冲区
字符流使先将数据存储到缓存中,然后再将修改加到文件中
解决缓冲
flush()
bio和nio
bio:同步阻塞
nio:同步非阻塞
aio:异步非阻塞(异步是读写操作,使用缓冲区)
1.同步:使用同步IO时,Java自己处理IO读写。
2.异步:使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,完成后OS通知Java处理(回调)。
3.阻塞:使用阻塞IO时,Java调用会一直阻塞到读写完成才返回。
4.非阻塞:使用非阻塞IO时,如果不能立马读写,Java调用会马上返回,当IO事件分发器通知可读写时在进行读写,不断循环直到读写完成。
BIO(同步,阻塞,面向流)
- 概述:bio是通过io流来操作数据的,io流按操作数据分为:1.字节流,字符流 按流向分类:输入和输出
- 字符流:
1.只是用来处理文本数据,常见的表现形式是文件
2.注意事项:写入时要用flush()刷新; 完结时需要关闭 - 字节流:
1.用来处理媒体数据数据,因为操作的是字节,而媒体文件也是以字节存储的。
2.操作可以不用刷新流操作 - 面向流意味着从只能从流中顺序的读取一个或多个字节,如果需要跳过一些字节或者重新读取已经读过的字节,你必须将流中读取的数据先缓存起来
NIO(同步,非阻塞)
- 概述:NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程,nio主要有三个组成部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
channel(通道)
Channel(通道):Channel是一个对象,可以通过它读取和写入数据。可以把它看做是IO中的流,不同的是:
1)Channel是双向的,既可以读又可以写,而流是单向的
2)Channel可以进行异步的读写
3)对Channel的读写必须通过buffer对象
正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。
在Java NIO中的Channel主要有如下几种类型:
FileChannel:从文件读取数据的
DatagramChannel:读写UDP网络协议数据
SocketChannel:读写TCP网络协议数据
ServerSocketChannel:可以监听TCP连接
Buffer(缓存区)
- 概述:在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数组,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。
使用 Buffer 读写数据一般遵循以下四个步骤:
1.写入数据到 Buffer;
2.调用 flip() 方法;(filp()使buffer读写转换,其实就是改变在缓冲区中的操作的位置等属性)
3.从 Buffer 中读取数据;
4.调用 clear() 方法或者 compact() 方法
flip()方法:将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值;
清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer主要有如下几种:
ByteBuffer ; CharBuffer; DoubleBuffer等…
- 重要标志:
buffer的大小/容量 - Capacity
作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。
当前读/写的位置 - Position
当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。
信息末尾的位置 - limit
- 例子
public static void copyFileUseNIO(String src,String dst) throws IOException{
//声明源文件和目标文件
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
//获得传输通道channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
//获得容器buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
//判断是否读完文件
int eof =inChannel.read(buffer);
if(eof==-1){
break;
}
//重设一下buffer的position=0,limit=position
buffer.flip();
//开始写
outChannel.write(buffer);
//写完要重置buffer,重设position=0,limit=capacity
buffer.clear();
}
inChannel.close();
outChannel.close();
fi.close();
fo.close();
}
Selector(选择器对象)
- 概述:因为线程上下文切换的开销在高并发时变得很大,也就是同步阻塞的低扩展性劣势,所以就有了selector,可以将多个channel注册到selector中,集中管理监听,这样子就可以利用一个线程来处理多个channels,相当于线程pool等。
- 创建并注册模板
//1.首先创建一个选择器
Selector selector = Selector.open();
//注册的Channel 必须设置成异步模式 才可以,否则异步IO就无法工作,这就意味着我们不能把一个FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的SocketChannel是可以的。
channel.configureBlocking(false);
//将channel注册到选择器中,并且让选择器监听channel的read事件
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
- 重要属性SelectionKey
该属性包含以下字段,
- The interest set: 就是你监听的channel的特定事件 eg:SelectionKey.OP_READ
- The ready set:已经准备就绪的操作的集合
- The Channel: 获取注册的channel
- The Selector: 获取选择器
- An attached object (optional): 绑定在该属性的对象
AIO(异步,非阻塞)
- 概述:AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。
但是对AIO来说,则更加进了一步,==它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。==因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
为什么需要NIO
高并发量引起的问题,一个使用传统阻塞I/O的系统,如果还是使用传统的一个请求对应一个线程这种模式,一旦有高并发的大量请求,就会有如下问题:
1、线程不够用, 就算使用了线程池复用线程也无济于事;
2、阻塞I/O模式下,会有大量的线程被阻塞,一直在等待数据,这个时候的线程被挂起,只能干等,CPU利用率很低,换句话说,系统的吞吐量差;
3、如果网络I/O堵塞或者有网络抖动或者网络故障等,线程的阻塞时间可能很长。整个系统也变的不可靠;
mappedByteBuffer和ByteBuffer的特色
各自读取文件流程
- BIO :ByteBuffer数据要多次拷贝;先从页面文件–>物理内存(堆外内存)–>堆内存(虚拟机)–>操作
- NIO和BIO :ByteBuffer从页面文件–>物理内存(堆外内存)–>操作
- mappedByteBuffer:从页面文件–>物理内存(堆外内存)–>操作
这里也可以知道NIO的性能也很不错.但是在不考虑内存占用的情况下,一次性读取入内存的mappedByteBuffer会比NIO还要快些.
map过程
FileChannel提供了map方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。
FileChannel中的几个变量:
MapMode mode:内存映像文件访问的方式,共三种:
MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。
MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。
MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是buffer自身的改变,这种能力称之为”copy on write”。
position:文件映射时的起始位置。
allocationGranularity:Memory allocation size for mapping buffers,通过native函数initIDs初始化。
MappedByteBuffer优缺点
- MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。
- 如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容。
- MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
javadoc中也提到:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.*
大文件上传
项目源码:https://github.com/jiaojiaoyow/git_demo/tree/master/big_file
一.遇到的坑:
- 执行mappedByteBuffer.put(fileData)报【with root cause java.nio.ReadOnlyBufferException: null】错误
原因是:
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,offset,fileData.length);
中的MapMode.READ_ONLY写错了,应该改成READ_WRITE