等不及,冲滴滴去了!
大家好,我是小林。
发现现在校招同学都好卷,为了能毕业找到不错的工作,在校期间都会积累多段实习经历,甚至有同学大二开始卷实习经历了。
从结果来看,确实有实习经历的同学,在找工作的时候,也会更顺利一些。有了实习经历后,在秋招过程中也会比较顺利,积累多个中大实习的话,秋招获得 sp、ssp offer 的概率会比较大一些。
对于大厂的实习面试,还是八股文+项目+算法这三大块的面试范围。
对于Java 同学来说,Java 基础+Java 并发+Java 容器+JVM+MySQL+Redis+1~2 个后端项目+网络+系统+算法,这几大块知识的常规八股文准备好,就基本可以尝试投实习了,通常来说越早投实习,越不卷,通过率也会比较高。
所以,25 届的同学可以准备学习,准备找实习了,很多 25 届还不知道面试的具体范围。
今天就来分享一位研二 Java 同学面试滴滴实习的面经,主要是问了Java+Redis&MySQL+系统&网络+算法+项目,都是比较经典面试题,收藏起来,反复复习!
操作系统
线程和协程有什么区别?
调度方式:线程的调度由操作系统内核负责,采用的是抢占式调度,即操作系统可以主动剥夺线程的执行权。而协程的调度由用户程序控制,采用的是协作式调度,即协程主动让出执行权。 切换开销:线程切换需要从用户态切换到内核态,涉及CPU寄存器的保存和恢复等操作,开销较大。而协程切换是在用户态进行的,开销较小。 内存开销:线程的创建和销毁涉及操作系统的调用和资源分配,开销较大。而协程的创建和销毁由用户程序控制,开销较小。
为什么协程切换的开销比线程切换小?
用户态切换:协程的切换是在用户态进行的,不需要操作系统的介入。相比之下,线程的切换需要操作系统进行调度和上下文切换,需要从用户态切换到内核态,这涉及到CPU寄存器的保存和恢复等操作,开销较大。 协作式调度:协程的调度是协作式的,由协程自身主动让出执行权,而不是被操作系统强制切换。这种调度方式避免了不必要的上下文切换,减少了切换开销。
进程和线程的区别?
本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
举个例子:进程=火车,线程=车厢
线程在进程下行进(单纯的车厢无法运行) 一个进程可以包含多个线程(一辆火车可以有多个车厢) 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
多线程是不是越多越好,太多会有什么问题?
多线程不一定越多越好,过多的线程可能会导致一些问题。
切换开销:线程的创建和切换会消耗系统资源,包括内存和CPU。如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃。 死锁的问题:过多的线程可能会导致竞争条件和死锁。竞争条件指的是多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是指多个线程相互等待对方释放资源,导致程序无法继续执行。
知道fork吗?
知道的,fork是创建子进程的系统调用方法。
#include <unistd.h>
int main() {
pid_t child_pid;
child_pid = fork();
if (child_pid == 0) {
// 子进程逻辑
} else if (child_pid > 0) {
// 父进程逻辑
} else {
// fork失败的处理逻辑
}
return 0;
}
多个进程的资源是共享的还是隔离的?
多个进程的资源是隔离的。每个进程有自己独立的内存空间,不能直接访问其他进程的内存。进程也有自己的文件描述符表、网络连接等资源,这些资源也是独立的,不会被其他进程访问或影响。
一个进程的所有内存资源对于线程都是共享的吗?
在同一个进程中的多个线程共享相同的内存空间,包括代码段、数据段、堆和共享库等。这意味着线程可以直接访问进程的全局变量、静态变量和动态分配的内存等资源。
不过,线程也有自己的栈空间。每个线程在栈上分配自己的局部变量和函数调用信息,这些是线程私有的。栈空间在每个线程之间是隔离的,不会被其他线程访问。这意味着每个线程拥有自己的栈帧和栈指针。
一个进程fork出一个子进程,那么他们占用的内存是之前的2倍吗?
不是的。
fork 的时候,创建的子进程是复父进程的虚拟内存,并不是物理内存,这时候父子的虚拟内存指向的是同一个物理内存空间,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制」。
写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存。
网络
DNS是什么?
DNS主要的作用是将将域名(例如www.example.com)转换为对应IP地址(例如192.0.2.1)。它充当了互联网上的电话簿,将人们熟悉的域名转换为机器可识别的IP地址。
域名解析的工作流程
客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。” 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?” 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。
至此,我们完成了 DNS 的解析过程。现在总结一下,整个过程我画成了一个图。
DNS域名解析使用的什么协议?
DNS域名解析使用的是UDP协议。在DNS中,域名解析请求和响应都是基于UDP进行传输的。
UDP是一种无连接的、不可靠的传输层协议,它提供了一种简单的传输机制,适用于对实时性要求较高的应用场景。DNS使用UDP协议进行域名解析是因为域名解析通常是短小而频繁的请求,UDP的无连接特性可以减少建立和断开连接的开销,并提高解析的效率。
UDP对于TCP有什么缺点?在这个业务里怎么解决?
UDP对于TCP的缺点是没办法保证数据的可靠传输,针对这个缺陷,可以在应用层实现一个超时重传机制,如果域名解析请求在一定时间内没收到响应,那么就重发域名解析请求。
ping命令用的什么协议?
用的是ICMP协议。
ICMP
主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。
它为什么用ICMP不用UDP?
ICMP是在网络层,UDP是在传输层。
因为ping命令是不需要传输数据的,只需要一个 true/false 的结果,所以根本没必要用传输层的UDP协议。
Redis&MySQL
Redis为什么这么快?
官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:
之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了; Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。 Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
建立联合索引有什么需要注意的?
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。
区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:
比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是"30%")很高的时候,它一般会忽略索引,进行全表扫描。
mysql索引失效原因有哪些?
当我们使用左或者左右模糊匹配的时候,也就是 like %xx
或者like %xx%
这两种方式都会造成索引失效;当我们在查询条件中对索引列使用函数,就会导致索引失效。 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
索引优化方式有哪些?
前缀索引优化:前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
覆盖索引优化;覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
主键索引最好是自增的:InnoDB 创建主键索引默认为聚簇索引,数据被存放在了 B+Tree 的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。 如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。 索引最好设置为 NOT NULL:为了更好的利用索引,索引列要设置为 NOT NULL 约束。有两个原因:
第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。
第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中至少会用 1 字节空间存储 NULL 值列表,如下图的紫色部分:
Java
Java变量在内存各个区域的分布
栈(Stack):栈是用于存储方法执行时的局部变量、方法参数和方法调用的上下文信息的内存区域。栈的大小在编译时确定,是线程私有的,每个线程都有自己的栈空间。栈的分配和释放是自动进行的,随着方法的调用和返回而动态变化。 堆(Heap):堆是用于动态分配对象的内存区域。在堆中创建的对象由垃圾回收器负责回收。堆的大小可以通过JVM参数进行调整,是线程共享的。 方法区(Method Area):方法区是用于存储类信息、静态变量、常量、编译器优化后的代码等数据的内存区域。方法区也是线程共享的,它的大小可以通过JVM参数进行调整。 程序计数器(Program Counter):程序计数器是用于记录当前线程执行的字节码指令的地址的内存区域。每个线程都有自己的程序计数器,用于指示线程执行的位置。 本地方法栈(Native Method Stack):本地方法栈是用于存储本地方法(Native Method)执行时的局部变量和方法调用的上下文信息的内存区域。本地方法栈的分配和释放与栈类似,是线程私有的。
把局部变量放到堆里会有什么问题?
内存泄漏:如果局部变量被放置在堆中,且没有正确地进行释放或管理,可能会导致内存泄漏。内存泄漏指的是不再使用的对象仍然存在于内存中,无法被垃圾回收器回收,从而占用了宝贵的内存资源。 性能降低:将局部变量放在堆中会增加垃圾回收的负担。垃圾回收器需要扫描堆中的对象,找到不再使用的对象进行回收。如果堆中存在大量的局部变量对象,垃圾回收的时间会增加,可能会导致程序的性能下降。
把对象动态分配到栈中会有什么问题?
生命周期限制:栈中的对象的生命周期与其所在的方法调用相关联。当方法调用结束时,栈中的对象会自动释放,无法在方法之外访问。如果需要在方法之外继续使用对象,就无法将其放置在栈中。 空间限制:栈的大小是有限的,并且在编译时就确定了。如果对象较大或者栈空间较小,将对象放置在栈中可能会导致栈溢出的问题。 不适用于共享和跨方法访问:栈是线程私有的,栈中的对象无法被其他线程或其他方法访问。如果需要在多个方法之间共享对象,或者在方法调用之外访问对象,将其放置在栈中是不可行的。
其他
项目问题:项目里有什么亮点
算法:K个一组反转链表
历史好文: