大家好,我是二哥呀。
公司新来的小姑娘买车了,今天试坐了一把,比我的买菜车舒服多了,是理想的电动车,不得不说,现在这自动泊车技术是真的厉害。
每次坐电车,最大的感慨就是它停车真的很标准。可惜我没有换车的诉求,买车 8 年,就跑了 5 万多公里(🤣)
来看看新能源理想汽车今年应届生给出的薪资吧,我也同步了一些星球、offershow 和牛客上的数据,方便大家做个参考。
-
博士 985,大模型岗位,给到了 80 万的年包,但有些担心 16 薪能不能拿满?
-
硕士 985,嵌入式开发,给到了 32k,公积金双边都是 12%,base 上海
-
硕士 985,后端开发,给到了 28k,一点都不比互联网低,感觉很理想了
-
硕士海龟,后端岗位,给到了 25k,北京地区,算是 SP 吧?
我对比了一下去年理想汽车同样岗位同样学历的薪资,还真差不多,说明理想汽车今年应该是平稳发展,李想同学又可以去买一辆法拉利了。
截图来自360汽车频道
那接下来,我们就以
Java 面试指南
中收录的理想汽车面经同学 2 一面为例,来看看如果想备战理想这种车企后端开发岗面试的话,应该如何去准备。
背八股就认准三分恶的面渣逆袭
从这份面经能看得出来,理想汽车的 Java 岗还是挺能问的,和互联网大厂的难度不相上下。
-
-
2、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
理想汽车面经同学 2 一面
在项目中主要负责什么?
技术派是和三个宿友做的,我主要负责后端的接口开发,一名宿友负责前端,还有一名宿友负责 admin 端。
PmHub 是一个微服务项目,主要用到了 Spring Cloud、Nacos、Gateway、Seata、Sentinel 等技术栈。技术派是一个前后端分离的单体项目,本来二期是想做微服务改造的,后来我就想,不如直接再做一个新的业务吧,项目管理、OA 审批属于很多公司都会商用的项目,于是就又一起做了这个 PmHub,同样是我们三个人,我还是负责项目搭建、后端接口开发。
性能调优遇到了什么瓶颈,以及是如何优化的?
遇到最大的问题,我想应该就是如何加速用户访问技术派首页的速度,因为我们的服务器是一个丐版,只有 1M 带宽 4G 内存 2 核 CPU,我记得第一版的时候,首页完全加载完需要消耗 4 秒左右。
后来经过一系列的努力,比如说前端静态资源通过 Nginx 进行缓存、压缩、CDN 分发,后端返回结果通过 Spring Boot 压缩,接口请求数据时串行改并行,加本地缓存 Caffeine 以及分布式缓存 Redis,合理的库表设计(如索引、分页)等。
速度得到了极大改善,目前首页完全加载完不到 1 秒,对于用户来说完全无感知,非常快。
Redis在项目中起到了什么作用?
技术派中用到 Redis 的地方还是蛮多的,比如说我们使用 Redis 来缓存 session、sitemap和导航栏菜单等热点数据等,另外,还用了 zset 来做白名单和用户活跃榜排行榜。
除了Redis锁实现分布式锁,还有别的方法吗?
还可以通过引入 Redission 的看门狗算法来实现分布式锁,这样就可以一劳永逸了。
说说你对GC的了解?
垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。
JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。
二哥的 Java 进阶之路:可达性分析
在确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。
技术派项目使用的 JDK 8,所以默认采用的是 CMS 垃圾收集器。
了解过G1垃圾回收器吗?
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
有梦想的肥宅:G1 收集器
G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代(Eden 和 Survivor)或老年代的角色。
同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。
这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。
了解volatile吗?
volatile 关键字主要有两个作用,一个是保证变量的内存可见性,一个是禁止指令重排序。它确保一个线程对变量的修改对其他线程立即可见,同时防止代码执行顺序被编译器或 CPU 优化重排。
追问:在汇编语言层面是如何实现的?
当线程对 volatile 变量进行写操作时,JMM 会在写入这个变量之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。
三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图
在 x86 架构下,volatile 写操作会插入一个 lock 前缀指令,这个指令会将缓存行的数据写回到主内存中,确保内存可见性。
mov [a], 2 ; 将值 2 写入内存地址 a
lock add [a], 0 ; lock 指令充当写屏障,确保内存可见性
当线程对 volatile 变量进行读操作时,JMM 会插入一个 读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。
三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图
synchronized VS ReentrantLock VS CAS
synchronized 是一个关键字,ReentrantLock是 Lock 接口的一个实现。
三分恶面渣逆袭:synchronized和ReentrantLock的区别
它们都可以用来实现同步,但也有一些区别:
-
ReentrantLock 可以实现多路选择通知(绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知);
-
ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放;synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放,不需要手动操作。
-
ReentrantLock 通常能提供更好的性能,因为它可以更细粒度控制锁;synchronized 只能同步代码快或者方法,随着 JDK 版本的升级,两者之间性能差距已经不大了。
CAS 是一种乐观锁的实现方式,全称为“比较并交换”(Compare-and-Swap),是一种无锁的原子操作。
synchronized 是悲观锁,尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多,但依然是悲观锁,线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。
CAS 是乐观锁,线程执行的时候不会加锁,它会假设此时没有冲突,然后完成某项操作;如果因为冲突失败了就重试,直到成功为止。
CAS 原子性:博客园的紫薇哥哥
JAVA中线程池有哪些?
可以通过 Executors 工厂类来创建四种线程池:
-
newFixedThreadPool (固定线程数目的线程池)
-
newCachedThreadPool (可缓存线程的线程池)
-
newSingleThreadExecutor (单线程的线程池)
-
newScheduledThreadPool (定时及周期执行的线程池)
线程池淘汰策略
主要有四种:
-
AbortPolicy:这是默认的拒绝策略。该策略会抛出一个 RejectedExecutionException 异常。
-
CallerRunsPolicy:该策略不会抛出异常,而是会让提交任务的线程(即调用 execute 方法的线程)自己来执行这个任务。
-
DiscardOldestPolicy:策略会丢弃队列中最老的一个任务(即队列中等待最久的任务),然后尝试重新提交被拒绝的任务。
-
DiscardPolicy:策略会默默地丢弃被拒绝的任务,不做任何处理也不抛出异常。
三分恶面渣逆袭:四种策略
追问:可以自定义淘汰策略吗?淘汰策略的实现类是啥?
如果默认策略不能满足需求,可以通过自定义实现 RejectedExecutionHandler 接口来定义自己的淘汰策略。例如:记录被拒绝任务的日志
class CustomRejectedHandler {
public static void main(String[] args) {
// 自定义拒绝策略
RejectedExecutionHandler rejectedHandler = (r, executor) -> {
System.out.println("Task " + r.toString() + " rejected. Queue size: "
+ executor.getQueue().size());
};
// 自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
10, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), // 阻塞队列容量
Executors.defaultThreadFactory(),
rejectedHandler // 自定义拒绝策略
);
for (int i = 0; i 10; i++) {
final int taskNumber = i;
executor.execute(() -> {
System.out.println("Executing task " + taskNumber);
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
什么操作会导致索引失效?
-
在索引列上使用函数或表达式
:索引可能无法使用,因为 MySQL 无法预先计算出函数或表达式的结果。例如:
SELECT * FROM table WHERE YEAR(date_column) = 2021
。
-
使用不等于(
<>
)或者 NOT 操作符:因为它们会扫描全表。
-
使用 LIKE 语句,但通配符在前面
:以“%”或者“_”开头,索引也无法使用。例如:
SELECT * FROM table WHERE column LIKE '%abc'
。
-
联合索引,但 WHERE 不满足最左前缀原则,索引无法起效。例如:
SELECT * FROM table WHERE column2 = 2
,联合索引为
(column1, column2)
。
Spring AOP的概念了解吗?
AOP,也就是面向切面编程,简单点说,AOP 就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。
三分恶面渣逆袭:横向抽取
AOP和OOP的关系?
AOP 和 OOP 是互补的编程思想:
-
OOP 通过类和对象封装数据和行为,专注于核心业务逻辑。
-
AOP 提供了解决横切关注点(如日志、权限、事务等)的机制,将这些逻辑集中管理。
了解AOP底层是怎么做的吗?
AOP 是通过
动态代理
实现的,代理方式有两种:JDK 动态代理和 CGLIB 代理。
①、JDK 动态代理是基于接口的代理,只能代理实现了接口的类。
使用 JDK 动态代理时,Spring AOP 会创建一个代理对象,该代理对象实现了目标对象所实现的接口,并在方法调用前后插入横切逻辑。
②、CGLIB 动态代理是基于继承的代理,可以代理没有实现接口的类。
使用 CGLIB 动态代理时,Spring AOP 会生成目标类的子类,并在方法调用前后插入横切逻辑。
图片来源于网络
AOP的使用场景有哪些?
AOP 的使用场景有很多,比如说日志记录、事务管理、权限控制、性能监控等。
我在技术派实战项目中主要利用 AOP 来打印接口的入参和出参日志、执行时间,方便后期 bug 溯源和性能调优。
沉默王二:技术派教程
如何理解缓存雪崩、缓存击穿和缓存穿透?
缓存穿透、缓存击穿和缓存雪崩是指在使用 Redis 做缓存时可能遇到的三种高并发场景下的问题。
缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。
三分恶面渣逆袭:缓存击穿
缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。
三分恶面渣逆袭:缓存穿透
客户端请求某个 ID 的数据,首先检查缓存是否命中。如果缓存未命中,查询数据库。如果数据库查询结果为空,将该空结果(如 null 或 {})缓存起来,并设置一个合理的过期时间。当后续请求再访问相同 ID 时,缓存直接返回空结果,避免每次都打到数据库。
三分恶面渣逆袭:缓存空值/默认值
追问:说明一下布隆过滤器
布隆过滤器是一种空间效率极高的概率型数据结构,用于快速检查一个元素是否存在于一个集合中。
三分恶面渣逆袭:布隆过滤器
布隆过滤器由一个长度为 m 的位数组和 k 个哈希函数组成。
-
-
当一个元素被添加到过滤器中时,它会被 k 个哈希函数分别计算得到 k 个位置,然后将位数组中对应的位设置为 1。
-
当检查一个元素是否存在于过滤器中时,同样使用 k 个哈希函数计算位置,如果任一位置的位为 0,则该元素肯定不在过滤器中;如果所有位置的位都为 1,则该元素可能在过滤器中。