基础IO
1.如何从数据传输方式理解IO流?
IO流根据处理数据的类型可以分为字节流和字符流。
字节流
字节流以字节(8位)为单位读写数据。字节流主要用于读写二进制文件,如图片、音频、视频等。Java中的InputStream和OutputStream就是字节流。
InputStream和OutputStream的子类有很多,如FileInputStream和FileOutputStream、ByteArrayInputStream和ByteArrayOutputStream等。使用字节流进行读写文件时,一般需要使用缓冲流来提高读写效率。缓冲流会将数据先缓存到内存中,再批量读写,从而避免了频繁的磁盘读写操作。
字符流
字符流以字符为单位读写数据。字符流主要用于读写文本文件,如txt文件等。Java中的Reader和Writer就是字符流。
Reader和Writer的子类有很多,如FileReader和FileWriter、CharArrayReader和CharArrayWriter等。使用字符流进行读写文件时,一般也需要使用缓冲流来提高读写效率。需要注意的是,使用字符流读写文件时需要注意文件编码的问题,否则可能会出现乱码。
在实际开发中,如果需要读写的数据是二进制数据,应该使用字节流;如果需要读写的数据是文本数据,应该使用字符流。需要根据实际需求选择合适的流进行操作。
字节流
字符流
字节转字符?
2.如何从数据操作上理解IO流?
3.Java IO设计上使用了什么设计模式?
在Java IO中,装饰器模式被广泛应用于输入流和输出流的设计中。以输入流为例,下面是一个使用装饰器模式的简单示例:
InputStream input = new FileInputStream("file.txt");
input = new BufferedInputStream(input);
input = new DataInputStream(input);
在这个示例中,我们首先创建了一个FileInputStream对象,它是一个具体组件,表示一个输入流。然后,我们通过装饰器模式创建了一个缓冲输入流对象,它是一个具体装饰器,向原始对象添加了缓冲功能。最后,我们又通过装饰器模式创建了一个数据输入流对象,它也是一个具体装饰器,向缓冲输入流添加了读取数据的功能。这样,我们就可以通过数据输入流对象来读取文件中的数据了。
在输出流中,同样使用了装饰器模式。下面是一个简单的示例:
OutputStream output = new FileOutputStream("file.txt");
output = new BufferedOutputStream(output);
output = new DataOutputStream(output);
在这个示例中,我们首先创建了一个FileOutputStream对象,它是一个具体组件,表示一个输出流。然后,我们通过装饰器模式创建了一个缓冲输出流对象,它是一个具体装饰器,向原始对象添加了缓冲功能。最后,我们又通过装饰器模式创建了一个数据输出流对象,它也是一个具体装饰器,向缓冲输出流添加了写入数据的功能。这样,我们就可以通过数据输出流对象来将数据写入到文件中了。
5种IO模型
1.什么是阻塞?什么是同步?
阻塞IO 和 非阻塞IO
这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
同步IO 和 非同步IO
这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
2.什么是同步阻塞IO?
同步阻塞IO是指程序进行IO操作时,程序会一直等待,直到操作完成,期间无法进行其他操作。例如,在Java中,如果使用InputStream的read()方法进行读操作,当没有可读数据时,该方法将会一直阻塞线程,直到有可读数据或者读操作超时才返回。
同步阻塞IO通常使用单线程或者有限数量的线程来处理IO操作,每个线程会阻塞在IO操作上,等待IO操作完成后才会继续执行。这种模型的优点是简单可靠,适用于并发请求量不大的场景,但是并发能力有限,无法满足高并发的需求。
同步阻塞IO的一个特点是每个IO操作需要独立的线程或者进程来处理,因此在并发访问时,可能会出现线程竞争、线程切换等问题,导致性能下降。此外,由于每个线程都会阻塞在IO操作上,当IO操作非常耗时时,会导致线程资源浪费,影响程序的响应性能。
3.什么是同步非阻塞IO?
同步非阻塞IO是指程序进行IO操作时,程序不会一直阻塞等待操作完成,而是先向操作系统发起一个IO请求,并继续执行其他任务。当IO操作完成时,操作系统会通知程序,并将数据复制到程序缓冲区中,程序再次请求读取或写入缓冲区中的数据。
在同步非阻塞IO模型中,程序不会等待IO操作完成,而是通过轮询或者事件通知的方式查询IO状态,直到IO操作完成或者超时才返回。这种模型适用于并发请求数较多的场景,可以通过少量线程处理大量并发请求,从而提高并发处理能力。
同步非阻塞IO的一个特点是需要程序不断轮询IO状态或者注册IO事件,并且需要不断地检查IO状态。这种模型需要更多的CPU资源来处理IO操作,但相对于同步阻塞IO模型,可以更好地支持并发请求。
4.什么是多路复用IO?
多路复用IO(Multiplexing IO)是一种IO模型,它通过使用一种机制,使得单个进程可以同时处理多个I/O请求。
在多路复用IO模型中,一个进程可以同时监听多个文件描述符,当有一个或多个文件描述符有I/O事件发生时,通过操作系统提供的系统调用进行通知,进程可以针对每个文件描述符进行处理,从而实现高并发的I/O处理。常见的多路复用IO机制包括select、poll和epoll。
多路复用IO的主要优点是能够在单个线程内处理多个I/O请求,相比于多线程或者多进程的方式,减少了线程或进程上下文切换的开销,避免了线程或进程资源的浪费。同时,由于多路复用IO可以同时处理多个I/O请求,能够有效地提高程序的并发处理能力,适用于高并发的网络应用场景。
总之,多路复用IO是一种高并发的IO模型,通过使用一种机制,使得单个进程可以同时处理多个I/O请求,能够有效地提高程序的并发处理能力,减少线程或进程资源的浪费。
5.有哪些多路复用IO?
常见的多路复用IO机制包括select、poll和epoll。
select:select是最早的多路复用IO机制之一,可以同时监听多个文件描述符,当有一个或多个文件描述符有I/O事件发生时,通过操作系统提供的系统调用进行通知,进程可以针对每个文件描述符进行处理。
poll:poll是select的改进版,同样可以监听多个文件描述符,但它采用链表来存储文件描述符,避免了select中文件描述符数量的限制。
epoll:epoll是Linux系统上的多路复用IO机制,相对于select和poll,epoll具有更高的效率和更低的延迟。它使用事件驱动的方式来处理文件描述符,可以同时处理大量的文件描述符,避免了select和poll中需要遍历所有文件描述符的缺点。同时,epoll还支持边缘触发和水平触发两种模式,能够更加灵活地处理I/O事件。
6.什么是信号驱动IO?
信号驱动IO是一种I/O模型,也称为异步I/O模型。在信号驱动IO模型中,当进行I/O操作时,进程会向操作系统注册一个信号处理函数,然后继续执行其他任务。当I/O操作完成后,操作系统会向进程发送一个信号,告知该I/O操作已经完成,进程可以通过信号处理函数来获取I/O操作的结果。
相对于阻塞和非阻塞IO模型,信号驱动IO模型更加高效,因为它可以同时处理多个I/O操作,而不需要阻塞进程或使用轮询等方式等待I/O操作的完成。同时,信号驱动IO模型相对于多路复用IO模型更加灵活,因为它不需要将多个文件描述符放在一个轮询列表中等待事件的发生,而是可以对每个文件描述符单独注册信号处理函数。
需要注意的是,信号驱动IO模型在某些情况下也可能存在一些缺陷。例如,当同时注册大量的信号处理函数时,会导致信号处理函数的调用时间变长,从而影响整个系统的性能。因此,在实际应用中,需要根据具体的情况选择最合适的IO模型。
7.什么是异步IO?
异步I/O是一种I/O模型,也称为事件驱动I/O。在异步I/O模型中,当进行I/O操作时,进程不会被阻塞,而是会向操作系统注册一个I/O操作,并继续执行其他任务。当I/O操作完成后,操作系统会通知进程,进程可以获取I/O操作的结果。
相对于阻塞、非阻塞和信号驱动IO模型,异步I/O模型更加高效,因为它可以充分利用CPU资源,同时处理多个I/O操作,而不需要等待任何一个操作的完成。异步I/O模型通常采用回调函数的方式来处理I/O操作的结果,当I/O操作完成后,操作系统会回调相应的函数,进程可以在回调函数中获取I/O操作的结果。
异步I/O模型通常用于需要处理大量I/O操作的系统中,如高并发的网络服务器、图形图像处理等领域。需要注意的是,异步I/O模型的实现比较复杂,需要使用操作系统提供的特定API或框架,如Linux系统中的epoll、Windows系统中的I/O Completion Ports等。
8.什么是Reactor模型?
Reactor模型是一种基于事件驱动的I/O模型,用于处理高并发的网络应用程序。它由Doug Lea等人在1996年提出,是一种被广泛应用的I/O编程模型。
Reactor模型的基本思想是将I/O事件的处理过程分离出来,使其在单独的线程中运行。这个线程被称为Reactor线程,它负责监听所有的I/O事件,并根据事件的类型分发给相应的事件处理器。事件处理器通常在单独的线程中执行,它负责处理I/O事件,并根据业务逻辑进行相应的处理。
Reactor模型中包含两个核心组件,即Reactor和Handler。Reactor是I/O事件处理器的核心,负责监听和分发所有的I/O事件。Handler是具体的事件处理器,它会在事件发生时被Reactor调用,并处理相应的I/O事件。
Reactor模型的优点在于能够有效地利用多核CPU资源,同时处理大量的并发请求,提高系统的吞吐量和响应速度。在实际应用中,Reactor模型被广泛应用于高并发的网络应用程序中,如Web服务器、消息队列等。常见的Reactor模型包括单线程Reactor模型、多线程Reactor模型和主从Reactor模型。
9.什么是Java NIO?
Java NIO的核心组件包括以下几个部分:
缓冲区(Buffer):用于存储数据的内存区域,可以通过NIO提供的ByteBuffer、CharBuffer、ShortBuffer等类来实现。
通道(Channel):用于对数据进行读写操作的对象,可以通过NIO提供的FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel等类来实现。
选择器(Selector):用于监控多个通道的I/O事件,可以通过NIO提供的Selector类来实现。
Java NIO相比于传统的Java I/O,具有以下几个优点:
非阻塞I/O操作:Java NIO提供了基于缓冲区、通道和选择器的非阻塞I/O操作方式,使得单线程可以同时处理多个I/O操作,提高了系统的并发处理能力。
支持多种网络协议:Java NIO提供了对TCP、UDP、SSL等多种网络协议的支持,可以满足不同场景下的需求。
零拷贝I/O:Java NIO支持零拷贝I/O操作,可以直接将数据从文件或网络中读取到内存中,提高了数据的传输效率。
在实际应用中,Java NIO被广泛应用于高并发的网络应用程序中,如网络服务器、消息队列等。
零拷贝
1.传统的IO存在什么问题?为什么引入零拷贝的?
传统的IO(也称为IO流或者普通IO)存在以下几个问题:
低效:传统的IO操作基于字节流或字符流进行,需要进行频繁的数据转换和复制,造成了大量的CPU和内存资源浪费,导致I/O操作效率低下。
阻塞:传统的IO操作是阻塞的,即读写操作必须等待I/O操作完成后才能继续执行,这导致单线程无法处理多个I/O操作,从而限制了系统的并发性能。
不支持零拷贝:传统的IO操作需要进行数据的复制和移动,即从文件或网络读取数据到内存中后,还需要将数据复制到另一个缓冲区中,再进行下一步操作。这种复制和移动操作浪费了大量的CPU和内存资源,影响了I/O操作的效率。
为了解决这些问题,引入了零拷贝技术。零拷贝(Zero-copy)是指在数据传输过程中,数据从磁盘、网络等外部设备直接传输到内存或者其他设备中,避免了数据的重复复制和移动,从而提高了I/O操作的效率和吞吐量。零拷贝技术可以避免数据复制和移动的过程,从而减少了CPU和内存的使用,提高了系统的性能。
Java NIO提供了零拷贝I/O操作的支持,通过使用DirectByteBuffer等直接缓冲区实现了数据在内存和外部设备之间的直接传输,避免了数据复制和移动的过程,从而提高了I/O操作的效率。
2.mmap + write怎么实现的零拷贝
mmap + write 是一种实现零拷贝的方式,其主要思路是将文件数据映射到内存中,通过内存映射的方式来访问文件数据,从而避免了数据从内核空间向用户空间的拷贝,以及从用户空间向内核空间的拷贝。具体实现步骤如下:
调用 mmap 函数将文件映射到内存中,得到一个指向内存的指针 buf。
调用 write 函数将内存中的数据写入网络套接字 sockfd 中。
在这个过程中,mmap 函数将文件的数据映射到了内存中,而 write 函数则直接将内存中的数据写入网络套接字中,避免了数据的拷贝过程。
需要注意的是,这里的写入操作是异步的,即 write 函数将数据写入套接字的缓冲区中后就返回了,不会等待数据发送完成。如果需要等待数据发送完成再进行下一步操作,需要使用相应的系统调用来实现。
相比传统的数据拷贝方式,mmap + write 可以避免多次数据拷贝,减少了 CPU 和内存的开销,提高了数据传输的效率。但也有一些限制和注意事项:
mmap 函数会将整个文件映射到内存中,因此对于大文件来说,可能会占用大量的内存,需要谨慎使用。
在进行写操作之前,需要确保文件的内容已经被完全读入内存中,否则写入的数据可能不完整。
对于频繁的写操作,由于每次写入都需要调用 mmap 函数重新映射内存,可能会带来额外的开销,因此需要根据具体情况进行优化。
3.sendfile怎么实现的零拷贝?
sendfile() 函数是一种系统调用,它在 Linux 和 Unix 系统上实现了零拷贝技术,可以在内核空间和用户空间之间直接传输数据,从而减少了数据拷贝的次数,提高了传输效率。
sendfile() 函数的基本用法如下
#include <sys/sendfile.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
其中,out_fd 是输出文件描述符,in_fd 是输入文件描述符,offset 是输入文件的偏移量,count 是要传输的字节数。在调用 sendfile() 函数时,输入文件描述符 in_fd 必须是一个打开的文件,而输出文件描述符 out_fd 必须是一个套接字。
sendfile() 函数的工作原理是这样的:在内核中,sendfile() 函数将输入文件描述符 in_fd 和输出套接字 out_fd 绑定在一起,然后在内核中将文件内容从输入文件描述符中复制到输出套接字中,这个过程是在内核中完成的,而不涉及用户空间。具体来说,sendfile() 函数首先在内核中将输入文件描述符和输出套接字建立一个连接,然后从输入文件描述符中读取数据到内核缓冲区中,再将数据从内核缓冲区中写入到输出套接字中,最后将输出套接字中写入的字节数返回给用户程序。
在使用 sendfile() 函数时,需要注意以下几点:
sendfile() 函数只能用于在两个文件描述符之间进行数据传输,而不能用于网络套接字和文件之间的传输。
sendfile() 函数只能用于 Linux 和 Unix 系统,而不能用于 Windows 系统。
sendfile() 函数只能传输一个文件的数据,如果需要传输多个文件,则需要在用户程序中多次调用 sendfile() 函数。
sendfile() 函数只能用于传输文件的全部内容,而不能用于传输文件的部分内容。
sendfile() 函数实现了零拷贝技术,可以在内核空间和用户空间之间直接传输数据,减少了数据拷贝的次数,提高了传输效率,是实现高性能网络编程的重要手段之一。