java - io知识泛讲(大文件上传例子)

参考文章

  1. java大文件上传例子:https://blog.csdn.net/j7s9usu/article/details/86678534
  2. bio-nio-aio部分知识参考:
    1)https://www.cnblogs.com/sxkgeek/p/9488703.html
    2)https://www.jianshu.com/p/362b365e1bcc
    3)https://blog.csdn.net/guanghuichenshao/article/details/79375967
  3. mappedByteBuffer和ByteBuffer的区别:
    1)https://blog.csdn.net/love4amanda/article/details/90412482
    2)https://blog.csdn.net/qq_41969879/article/details/81629469

知识点

  1. 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
  2. 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理

1. io流的分类

字节流和字符流的分类
字节输入流:InputStream,字节输出流:OutputStream
字符输入流:Reader,字符输出流:Writer

什么是字节流?

字节流–传输过程中,传输数据的最基本单位是字节的流。

什么是字符流?

字符流–传输过程中,传输数据的最基本单位是字符的流。

https://blog.csdn.net/chengyuqiang/article/details/79183748

字符流的缓冲区
https://blog.csdn.net/chenkaibsw/article/details/81606722
字符流使先将数据存储到缓存中,然后再将修改加到文件中
解决缓冲
flush()

bio和nio

https://blog.csdn.net/ty497122758/article/details/78979302
https://www.cnblogs.com/zedosu/p/6666984.html
bio:同步阻塞
nio:同步非阻塞
aio:异步非阻塞(异步是读写操作,使用缓冲区)

1.同步:使用同步IO时,Java自己处理IO读写。

2.异步:使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,完成后OS通知Java处理(回调)。

3.阻塞:使用阻塞IO时,Java调用会一直阻塞到读写完成才返回。

4.非阻塞:使用非阻塞IO时,如果不能立马读写,Java调用会马上返回,当IO事件分发器通知可读写时在进行读写,不断循环直到读写完成。

BIO(同步,阻塞,面向流)

  1. 概述:bio是通过io流来操作数据的,io流按操作数据分为:1.字节流,字符流 按流向分类:输入和输出
  2. 字符流:
    1.只是用来处理文本数据,常见的表现形式是文件
    2.注意事项:写入时要用flush()刷新; 完结时需要关闭
  3. 字节流:
    1.用来处理媒体数据数据,因为操作的是字节,而媒体文件也是以字节存储的。
    2.操作可以不用刷新流操作
  4. 面向流意味着从只能从流中顺序的读取一个或多个字节,如果需要跳过一些字节或者重新读取已经读过的字节,你必须将流中读取的数据先缓存起来

NIO(同步,非阻塞)

  1. 概述: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(缓存区)

  1. 概述:在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等…

  1. 重要标志:

buffer的大小/容量 - Capacity
作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。
当前读/写的位置 - Position​
当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。
信息末尾的位置 - limit

  1. 例子
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(选择器对象)

  1. 概述:因为线程上下文切换的开销在高并发时变得很大,也就是同步阻塞的低扩展性劣势,所以就有了selector,可以将多个channel注册到selector中,集中管理监听,这样子就可以利用一个线程来处理多个channels,相当于线程pool等。
  2. 创建并注册模板

//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);
  1. 重要属性SelectionKey

该属性包含以下字段,

  1. The interest set: 就是你监听的channel的特定事件 eg:SelectionKey.OP_READ
  2. The ready set:已经准备就绪的操作的集合
  3. The Channel: 获取注册的channel
  4. The Selector: 获取选择器
  5. An attached object (optional): 绑定在该属性的对象

AIO(异步,非阻塞)

  1. 概述: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的特色

各自读取文件流程

  1. BIO :ByteBuffer数据要多次拷贝;先从页面文件–>物理内存(堆外内存)–>堆内存(虚拟机)–>操作
  2. NIO和BIO :ByteBuffer从页面文件–>物理内存(堆外内存)–>操作
  3. 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优缺点

  1. MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。
  2. 如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容。
  3. 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

一.遇到的坑:

  1. 执行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
发布了36 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/s_xchenzejian/article/details/104051874