我分析了上百份大中小厂的面经,整理了 Java 面试中最最最常问的一些问题!小伙伴们可以对照着网站里面的文章学习或者准备面试。网站的内容会继续完善,欢迎你在评论区说出你遇到的高频面试题!
林老师带你学编程(「Java 学习+面试指南」是一份涵盖大部分 Java 程序员所需要掌握的核心知识网站,准备 Java 面试,首选《林老师带你学编程》。
什么是IO流?
它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取 数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。
java中有几种类型的流?
按照单位大小:字符流、字节流。按照流的方向:输出流、输入流。
字节流和字符流哪个好?怎么选择?
- 缓大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候 IO 操作都是直接操 作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)
- 如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能
读取数据量大的文件时,速度会很慢,如何选择流?
字节流时,选择BufferedInputStream和BufferedOutputStream。
字符流时,选择BufferedReader 和 BufferedWriter
IO模型有几种?
阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
阻塞IO (blocking IO)
应用程序调用一个IO函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一 直等待下去。 一个典型的读操作流程大致如下图,当用户进程调用recvfrom这个系统调用时, kernel就 开始了IO的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程(很多网络IO数据不会 那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据 从内核缓冲区拷贝到用户内存,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。
blocking IO 的特点就是在IO执行的两个阶段用户进程都会block住;
非阻塞I/O(nonblocking IO)
非阻塞I/O模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不 要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果 没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中, 会大量的占用CPU 的时间。
当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会block用户进程,而是立即返 回error ,用户进程判断结果是error ,就知道数据还没准备好,用户可以再次发read,直到kernel中数据 准备好,并且用户再一次发read操作,产生system call,那么kernel 马上将数据拷贝到用户内存,然后 返回;所以nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
阻塞IO一个线程只能处理一个IO流事件,要想同时处理多个IO流事件要么多线程要么多进程,这样 做效率显然不会高,而非阻塞IO可以一个线程处理多个流事件,只要不停地询所有流事件即可,当然这 个方式也不好,当大多数流没数据时,也是会大量浪费CPU资源;为了避免CPU空转,引进代理(select 和poll,两种方式相差不大),代理可以观察多个流I/O事件,空闲时会把当前线程阻塞掉,当有一个或 多个I/O事件时,就从阻塞态醒过来,把所有IO流都轮询一遍,于是没有IO事件我们的程序就阻塞在select方法处,即便这样依然存在问题,我们从select出只是知道有IO事件发生,却不知道是哪几个流, 还是只能轮询所有流, epoll这样的代理就可以把哪个流发生怎样的IO事件通知我们;
I/O多路复用模型(IO multiplexing)
I/O多路复用就在于单个进程可以同时处理多个网络连接IO,基本原理就是select ,poll ,epoll这些个函 数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个functon会阻 塞进程,但和IO阻塞不同,这些函数可以同时阻塞多个IO操作,而且可以同时对多个读操作,写操作IO 进行检验,直到有数据到达,才真正调用IO操作函数,调用过程如下图; 所以IO多路复用的特点是通过 一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就 绪状态, select函数就可以返回。
IO多路复用的优势在于并发数比较高的IO操作情况,可以同时处理多个连接,和bloking IO一样 socket是被阻塞的,只不过在多路复用中socket是被select阻塞,而在阻塞IO中是被socket IO给阻塞。
信号驱动I/O模型
可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,通过sigaction系统调用安装一个信号处 理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取 时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据
报,并通知主循环数据已经准备好待处理。特点:等待数据报到达期间进程不被阻塞。主循环可以继续 执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取
异步 I/O(asynchronous IO)
异步IO告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后 通知我们,调用aio_read (Posix异步I/O函数以aio或lio开头)函数,给内核传递描述字、缓冲区指针、 缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不 阻塞于等待I/0操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到 一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后, kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后, kernel会给用户进程发送一 个signal,告诉它read操作完成了
NIO与IO 的区别?
NIO即New IO,这个库是在JDK1.4中才引入的。 NIO和IO有相同的作用和目的,但实现方式不同,
NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO ,一套是针对标准 输入输出NIO,另一套就是网络编程NIO。
NIO和IO适用场景
NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长, NIO也有缺点,因为NIO是面向缓冲 区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断 缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据 处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。
那么NIO和IO各适用的场景是什么呢?
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器, 这时候用NIO处理数据可能是个很好的选择。而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处 理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
NIO核心组件
channel 、buffer 、selector
什么是channel
一个Channel (通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道 是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具 体的有FileChannel 、SocketChannel等。
通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。
当然,也有区别,主要体现在如下两点:一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream) 通道有非阻塞I/O模式
Java NIO中最常用的通道实现?
FileChannel:读写文件
DatagramChannel: UDP协议网络通信
SocketChannel : TCP协议网络通信
ServerSocketChannel:监听TCP连接
Buffer是什么?
NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以 灵活的操纵数据。
与Java基本类型相对应, NIO提供了多种 Buffer 类型,如ByteBuffer 、CharBuffer 、IntBuffer等,区别 就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
核心Buffer实现有哪些?
核心的buffer实现有这些: ByteBuffer 、CharBuffer 、DoubleBuffer 、FloatBuffer 、IntBuffer、
LongBuffer 、ShortBuffer,涵盖了所有的基本数据类型(4类8种,除了Boolean)。也有其他的buffer如 MappedByteBuffer。
buffer读写数据基本操作
1)、将数据写入buffer
2)、调用buffer.flip()
3)、将数据从buffer中读取出来
4)、调用buffer.clear()或者buffer.compact()
在写buffer的时候, buffer会跟踪写入了多少数据,需要读buffer的时候,需要调用flip()来将buffer从写 模式切换成读模式,读模式中只能读取写入的数据,而非整个buffer。
当数据都读完了,你需要清空buffer以供下次使用,可以有2种方法来操作:调用clear() 或者 调用 compact()。
区别: clear方法清空整个buffer ,compact方法只清除你已经读取的数据,未读取的数据会被移到 buffer的开头,此时写入数据会从当前数据的末尾开始。
// 创建⼀个容量为48的ByteBuffer
ByteBufferbuf=ByteBuffer.allocate(48);// 从channel中读(取数据然后写)⼊buffer
intbytesRead=inChannel.read(buf);
// 下⾯是读取buffer
while(bytesRead!=-1){
buf.flip(); // 转换buffer为读模式
System.out.print((char) buf.get()); // ⼀次读取⼀个byte
buf.clear(); //清空buffer准备下⼀次写⼊
}
Selector是什么?
Selector (选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册 到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道可以监听那几个事件?
通道有如下4个事件可供我们监听:
- Accept:有可以接受的连接
- Connect:连接成功
- Read:有数据可读
- Write:可以写入数据了
为什么要用Selector?
如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。 Selector 的出现解决了这施起的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select() 方法处被挂起,从而让出了CPU资源。
Selector处理多Channel 图文说明
要使用一个Selector,你要先注册这个Selector的Channels。然后你调用Selector的select()方法。这个方 法会阻塞,直到它注册的Channels当中有一个准备好了的事件发生了。当select()方法返回的时候,线程 可以处理这些事件,如新的连接的到来,数据收到了等。
林老师带你学编程(「Java 学习+面试指南」是一份涵盖大部分 Java 程序员所需要掌握的核心知识网站,准备 Java 面试,首选《林老师带你学编程!)。