大家好,我是二哥呀。
公司新来的小姑娘买车了,今天试坐了一把,比我的买菜车舒服多了,是理想的电动车,不得不说,现在这自动泊车技术是真的厉害。
每次坐电车,最大的感慨就是它停车真的很标准。可惜我没有换车的诉求,买车 8 年,就跑了 5 万多公里(🤣)
来看看新能源理想汽车今年应届生给出的薪资吧,我也同步了一些星球、offershow 和牛客上的数据,方便大家做个参考。
博士 985,大模型岗位,给到了 80 万的年包,但有些担心 16 薪能不能拿满? 硕士 985,嵌入式开发,给到了 32k,公积金双边都是 12%,base 上海 硕士 985,后端开发,给到了 28k,一点都不比互联网低,感觉很理想了 硕士海龟,后端岗位,给到了 25k,北京地区,算是 SP 吧?
我对比了一下去年理想汽车同样岗位同样学历的薪资,还真差不多,说明理想汽车今年应该是平稳发展,李想同学又可以去买一辆法拉利了。
那接下来,我们就以 Java 面试指南中收录的理想汽车面经同学 2 一面为例,来看看如果想备战理想这种车企后端开发岗面试的话,应该如何去准备。
从这份面经能看得出来,理想汽车的 Java 岗还是挺能问的,和互联网大厂的难度不相上下。
1、《30 天速通 Java.pdf》下载 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 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。
在确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。
技术派项目使用的 JDK 8,所以默认采用的是 CMS 垃圾收集器。
了解过G1垃圾回收器吗?
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代(Eden 和 Survivor)或老年代的角色。
同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。
这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。
了解volatile吗?
volatile 关键字主要有两个作用,一个是保证变量的内存可见性,一个是禁止指令重排序。它确保一个线程对变量的修改对其他线程立即可见,同时防止代码执行顺序被编译器或 CPU 优化重排。
追问:在汇编语言层面是如何实现的?
当线程对 volatile 变量进行写操作时,JMM 会在写入这个变量之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。
在 x86 架构下,volatile 写操作会插入一个 lock 前缀指令,这个指令会将缓存行的数据写回到主内存中,确保内存可见性。
mov [a], 2 ; 将值 2 写入内存地址 a
lock add [a], 0 ; lock 指令充当写屏障,确保内存可见性
当线程对 volatile 变量进行读操作时,JMM 会插入一个 读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。
synchronized VS ReentrantLock VS CAS
synchronized 是一个关键字,ReentrantLock是 Lock 接口的一个实现。
它们都可以用来实现同步,但也有一些区别:
ReentrantLock 可以实现多路选择通知(绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知); ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放;synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放,不需要手动操作。 ReentrantLock 通常能提供更好的性能,因为它可以更细粒度控制锁;synchronized 只能同步代码快或者方法,随着 JDK 版本的升级,两者之间性能差距已经不大了。
CAS 是一种乐观锁的实现方式,全称为“比较并交换”(Compare-and-Swap),是一种无锁的原子操作。
synchronized 是悲观锁,尽管随着 JDK 版本的升级,synchronized 关键字已经“轻量级”了很多,但依然是悲观锁,线程开始执行第一步就要获取锁,一旦获得锁,其他的线程进入后就会阻塞并等待锁。
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 个哈希函数组成。
开始时,布隆过滤器的每个位都被设置为 0。 当一个元素被添加到过滤器中时,它会被 k 个哈希函数分别计算得到 k 个位置,然后将位数组中对应的位设置为 1。 当检查一个元素是否存在于过滤器中时,同样使用 k 个哈希函数计算位置,如果任一位置的位为 0,则该元素肯定不在过滤器中;如果所有位置的位都为 1,则该元素可能在过滤器中。
线程内有哪些通信方式?线程之间有哪些通信方式?
线程之间传递信息有多种方式,比如说使用共享对象、wait()
和 notify()
方法、Exchanger 和 CompletableFuture。
①、使用共享对象,多个线程可以访问和修改同一个对象,从而实现信息的传递,比如说 volatile 和 synchronized 关键字。
②、**使用 wait() 和 notify()**,例如,生产者-消费者模式中,生产者生产数据,消费者消费数据,通过 wait()
和 notify()
方法可以实现生产和消费的协调。
一个线程调用共享对象的 wait()
方法时,它会进入该对象的等待池,并释放已经持有的该对象的锁,进入等待状态。
一个线程调用共享对象的 notify()
方法时,它会唤醒在该对象等待池中等待的一个线程,使其进入锁池,等待获取锁。
③、使用 Exchanger,Exchanger 是一个同步点,可以在两个线程之间交换数据。一个线程调用 exchange()
方法,将数据传递给另一个线程,同时接收另一个线程的数据。
④、使用 CompletableFuture,CompletableFuture 是 Java 8 引入的一个类,支持异步编程,允许线程在完成计算后将结果传递给其他线程。
class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟长时间计算
return "Message from CompletableFuture";
});
future.thenAccept(message -> {
System.out.println("Received: " + message);
});
}
}
有了解过Socket网络套接字吗?RPC框架相关的?
Socket 是网络通信的基础,表示两台设备之间通信的一个端点。Socket 通常用于建立 TCP 或 UDP 连接,实现进程间的网络通信。
RPC框架了解吗?
RPC是一种协议,允许程序调用位于远程服务器上的方法,就像调用本地方法一样。RPC 通常基于 Socket 通信实现。
RPC 框架支持高效的序列化(如 Protocol Buffers)和通信协议(如 HTTP/2),屏蔽了底层网络通信的细节,开发者只需关注业务逻辑即可。
输入www.baidu.com到浏览器显示出来的过程
以游客身份登陆视频网站刷视频,会有个性化推荐吗
即使以游客身份登录视频网站,网站也可以提供一定程度的个性化推荐:
比如说根据用户的 IP 地址推荐。 再比如说记录用户的浏览历史和观看行为,用于短期的个性化推荐。 还可以分析用户当前会话的点击和观看行为,生成即时推荐。
虽然没有注册用户的完整画像,但通过设备指纹和行为数据,可以在一定程度上推测用户兴趣,提供相关内容。
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 6600 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个 编程学习指南 + Java 项目实战 + LeetCode 刷题 + 简历精修 的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远。
欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。