BIO,NIO,AIO

BIO NIO AIO

1.什么是BIO?

bio:b为block,jdk 1.0 中的io体系是阻塞的
nio:n为non-block,针对block而言。就是非阻塞IO的意思
aio:a为asynchronous,异步的,异步io

发展历程:bio(jdk1.0) -> nio(jdk1.4) -> aio(jdk1.7)
所谓IO即input和output的缩写,是对数据的流入和流出的一种抽象,编程中很常见的一个概念。

image-20211219111907614

BIO就是食堂排队打饭,排队期间不能做别的事情。

由这四个类派生出来的子类名称都是以其父类名作为子类名的后缀,如InputStream的子类FileInputStream,Reader的子类FileReader。

程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
java中的阻塞式方法是指在程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。


什么是字节流?什么是字符流?

字节是占1个Byte,即8位;

字符是占2个Byte,即16位。

java的字节是有符号类型,而字符是无符号类型!


什么时候该用字节流,什么时候用字符流?

非纯文本数据:使用字节流(xxxStream)。比如读取图片 ,表情包

纯文本数据:使用字符流(xxxReader/xxxWriter),

最后其实不管什么类型文件都可以用字节流处理,包括纯文本,但会增加一些额外的工作量。所以还是按原则选择最合适的流来处理

小结:

1)判断操作的数据类型
纯文本数据:读用Reader系,写用Writer系
非纯文本数据:读用InputStream系,写用OutputStream系
如果纯文本数据只是简单的复制,下载,上传,不对数据内容本身做处理,那么使用Stream系
2)判断操作的物理节点
内存:ByteArrayXXX
硬盘:FileXXX
网络:http中的request和response均可获取流对象,tcp中socket对象可获取流对象
键盘(输入设备):System.in
显示器(输出设备):System.out
3)搞清读写顺序,一般是先获取输入流,从输入流中读取数据,然后再写到输出流中。
4)是否需增加特殊功能,如需要用缓冲提高读写效率则使用BufferedXXX,如果需要获取文本行号,则使用LineNumberXXX,如果需要转换流则使用InputStreamReader和OutputStreamWriter,如果需要写入和读取对象则使用ObjectOutputStream和ObjectInputStream


2.什么是NIO?

image-20211219141953296

Nio虽然还是银行取钱,但是是有一张小票,可以询问银行经理查看当前进度(是否轮到自己取钱了,当数据准备完毕时,就取钱的意思)

答:看了一些文章,传统的IO流是阻塞式的,会一直监听一个ServerSocket,在调用read等方法时,他会一直等到数据到来或者缓冲区已满时才返回。调用accept也是一直阻塞到有客户端连接才会返回。每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。并且多线程处理多个连接。每个线程拥有自己的栈空间并且占用一些 CPU 时间。每个线程遇到外部未准备好的时候,都会阻塞掉。阻塞的结果就是会带来大量的进程上下文切换。
对于NIO,它是非阻塞式,核心类:
1.Buffer为所有的原始类型提供 (Buffer)缓存支持。
2.Charset字符集编码解码解决方案
3.Channel一个新的原始 I/O抽象,用于读写Buffer类型,通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。

image-20211219141925650

3.什么是AIO?

image-20211219142531260

AIO有点像叫外卖的过程,先给店家打电话说想吃什么菜,店家回复知道了,这个时候电话以及挂了,等待店家准备好菜就送上门打电话给用户说要的以及到达了,可以开饭啦。

原理是基于事件回调机制。

image-20211219143355218

何为上下文切换?

​ 当一个线程的时间片用完后或者其他自身原因被迫暂停运行了,这时候,另外一个线程或者、进程或者其他进程的线程就会白操作系统选中,用来占用处理器。这种一个线程被暂停,一个线程包选中开始执行的过程就叫做上下文切换。

4.读写性能问题

​ 流的读写是比较耗时的操作,因此为了提高性能,便有缓冲的这个概念(什么是缓冲?假如你是个搬砖工,你工头让你把1000块砖从A点运到B点,你可以一次拿一块砖从A点运到B点放下砖,这样你要来回跑1000次,大多数的时间开销在路上了;你还可以使用一辆小车,在A点装满一车的砖,然后运到B点放下砖,如果一车最多可以装500块,那么你来回两次便可以把这些砖运完。这里的小车便是那个缓冲)。这里的装货可以理解成读操作,卸货可以理解成写操作。

​ 在java bio中使用缓冲一般有两种方式。一种是自己申明一个缓冲数组,利用这个数组来提高读写效率;另一种方式是使用jdk提供的处理流BufferedXXX类。下面我们分别演示不使用缓冲读写,使用自定义的缓冲读写,使用BufferedXXX缓冲读写一个文件。

无缓冲读写

    /**
     * 拷贝文件(方法一)
     * @param src 被拷贝的文件
     * @param dest 拷贝到的目的地
     */
    public static void copyByFileStream(File src,File dest){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        long start = System.currentTimeMillis();
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dest);
            int b = 0;
            while((b = fis.read()) != -1){//一个字节一个字节的读
                fos.write(b);//一个字节一个字节的写
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            close(fis,fos);
        }
        System.out.println("使用FileOutputStream拷贝大小"+getSize(src)+"的文件未使用缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒");
    }

自定义数组缓冲读写

    /**
     * 拷贝文件(方法二)
     * @param src 被拷贝的文件
     * @param dest 拷贝到的目的地
     * @param size 缓冲数组大小
     */
    public static void copyByFileStream(File src,File dest,int size){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        long start = System.currentTimeMillis();
        try {
            fis = new FileInputStream(src);
            fos = new FileOutputStream(dest);
            int b = 0;
            byte[] buff = new byte[size];//定义一个缓冲数组
            //读取一定量的数据(read返回值表示这次读了多少个数据)放入数组中
            while((b = fis.read(buff)) != -1){
                fos.write(buff,0,b);//一次将读入到数组中的有效数据(索引[0,b]范围的数据)写入输出流中
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            close(fos,fis);
        }
        System.out.println("使用FileOutputStream拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,生成的目标文件大小为"+getSize(dest));
    }

BufferedXXX类缓冲读写

    /**
     * 拷贝文件(方法三)
     * @param src
     * @param dest
     */
    public static void copyByBufferedStream(File src,File dest) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        long start = System.currentTimeMillis();
        try{
            bis = new BufferedInputStream(new FileInputStream(src));
            bos = new BufferedOutputStream(new FileOutputStream(dest));
            int b = 0;
            while( (b = bis.read())!=-1){
                bos.write(b);//使用BufferedXXX重写的write方法进行写入数据。该方法看似未缓冲实际做了缓冲处理
            }
            bos.flush();
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            close(bis,bos);
        }
        System.out.println("使用BufferedXXXStream拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒");
    }

方法测试:

public static void main(String[] args) {
    File src = new File("E:\\iotest\\1.bmp");
    File dest = new File("E:\\iotest\\1_copy.bmp");
    //无缓冲区
    copyByFileStream(src,dest);
    sleep(1000);
    //32字节缓冲区
    copyByFileStream(src,dest,32);
    sleep(1000);
    //64字节缓冲区
    copyByFileStream(src,dest,64);
    sleep(1000);
    //BufferedOutputStream缓冲区默认大小为8192字节  =1024*8   也就是8K缓冲区
    copyByBufferedStream(src, dest);
    sleep(1000);
    //BufferedOutputStream缓冲区默认大小为8192*2字节   16K缓冲区
    copyByBufferedStream(src, dest, 8192*2);
}

//我本地测试如下:
使用FileOutputStream拷贝大小864054字节的文件未使用缓冲数组耗时:5092毫秒,生成的目标文件大小为864054字节
使用FileOutputStream拷贝大小864054字节的文件使用了缓冲数组耗时:215毫秒,生成的目标文件大小为864054字节
使用FileOutputStream拷贝大小864054字节的文件使用了缓冲数组耗时:124毫秒,生成的目标文件大小为864054字节
使用BufferedXXXStream拷贝大小864054字节的文件使用了缓冲数组耗时:41毫秒,生成的目标文件大小为864054字节
使用BufferedXXXStream拷贝大小864054字节的文件使用了缓冲数组耗时:8毫秒,生成的目标文件大小为864054字节

5.熟悉select,pull,epull

Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll。

image-20211219142333694

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

我们熟悉的 select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件

select/poll/epoll 是如何获取网络事件的呢?在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。

select/poll

select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

1.拷贝到内核-

2.遍历+标记可读或科协

3.拷贝到用户态

4.遍历可读或可写

epoll-采用红黑树

epoll 通过两个方面,很好解决了 select/poll 的问题。

第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

红黑树记录待检测文件描述字。也就是入口控制,不需要遍历整个socket

第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

链表记录就绪事件,出口控制,不需要遍历整个socket

image-20211222153316142

什么是红黑树?

自平衡二叉查找树,实现关联数组。在插入和删除时,通过特定操作保持二叉查找树的平衡,从而获得较高查找性能

原文地址:https://www.cnblogs.com/yslu/p/15719707.html