大家好,我是二哥呀。
连续发了 4 家大厂的面经,包括网易、字节、虾皮和 Oppo,今天换个口味,体验一下中小厂的。
其实能去大厂的同学都是头部和部分肩部选手,真正能托举腰部和裆部选手就业的还是要看中小厂的招聘力度。
我也建议大家拓宽一下投递的范围,不要局限于大厂(至少先有个保底)。当然了,大、中、小具体怎么定义,没有国标,所以我改简历的时候碰到有球友把 B 站和小红书归到小公司的。
一般来说,官网有自己招聘通道的,可以算是大厂,中小厂可能就直接托管到老板直骗这种求职类网站上了。
这里给大家分享一个中厂名单,没有投过这些公司的同学,可以把简历投起来。包括好未来、名创优品、美图、掌趣科技、realme、学而思、宝宝巴士等等。
接下来,我们再来复盘一位球友的面经,他标注的是广州中厂,没说名字。不过大家看完后应该会有很多熟悉的题目映入眼帘。
要我说,背八股能力强的同学,最喜欢这种面试了,直接吟唱。。。
全是我强调的 Java 后端四大件中的内容,Java、MySQL、Redis 和 Spring,一个不缺,量大管饱😄
同学 6广州中厂面经
介绍实习做了什么
我主要参与了智能项目管理系统 PmHub 的开发,整个系统的架构分为:前端展示层、网关控制层、服务应用层、基础服务层、存储技术层、支撑服务层、运行资源层及 CI/CD。
我在项目中主要负责 project 模块的后端开发,该模块是 PmHub 的核心,可以自定义项目和任务,以及分配任务,还可以在任务中添加评论,任务完成后可以通过审批流进行任务流转,实现项目-任务之间的完美闭环。
在任务审批设置时,为了保证审批状态一致性,我使用了基于 Redis 的分布式锁,通过自定义注解+ AOP 完成了加锁和解锁的控制。
在添加任务时,需要添加成员、更新审批设置、更新日志等一系列操作,我通过 Seata 添加分布式事务,通过 @GlobalTransactional 注解保证添加任务时的事务一致性。
重写和重载
如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。
- 方法重载发生在同一个类中,同名的方法如果有不同的参数(参数类型不同、参数个数不同或者二者都不同)。
- 方法重写发生在子类与父类之间,要求子类与父类具有相同的返回类型,方法名和参数列表,并且不能比父类的方法声明更多的异常,遵守里氏代换原则。
HashMap的工作原理是怎么样的
JDK 8 中 HashMap 的数据结构是数组
+链表
+红黑树
。
数组用来存储键值对,每个键值对可以通过索引直接拿到,索引是通过对键的哈希值进行进一步的 hash()
处理得到的。
当多个键经过哈希处理后得到相同的索引时,需要通过链表来解决哈希冲突——将具有相同索引的键值对通过链表存储起来。
不过,链表过长时,查询效率会比较低,于是当链表的长度超过 8 时(且数组的长度大于 64),链表就会转换为红黑树。红黑树的查询效率是 O(logn),比链表的 O(n) 要快。
hash()
方法的目标是尽量减少哈希冲突,保证元素能够均匀地分布在数组的每个位置上。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果键的哈希值已经在数组中存在,其对应的值将被新值覆盖。
HashMap 的初始容量是 16,随着元素的不断添加,HashMap 就需要进行扩容,阈值是capacity * loadFactor
,capacity 为容量,loadFactor 为负载因子,默认为 0.75。
扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。
synchronized的锁状态和锁升级的过程
JDK 1.6 的时候,为了提升 synchronized 的性能,引入了锁升级机制,从低开销的锁可以最大程度减少锁的竞争。
没有线程竞争时,就使用低开销的“偏向锁”,此时没有额外的 CAS 操作;轻度竞争时,使用“轻量级锁”,采用 CAS 自旋,避免线程阻塞;只有在重度竞争时,才使用“重量级锁”,由 Monitor 机制实现,需要线程阻塞。
讲讲类加载过程
类从被加载到 JVM 开始,到卸载出内存,整个生命周期分为七个阶段,分别是载入、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析这三个阶段统称为连接。
除去使用和卸载,就是 Java 的类加载过程。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
创建线程的方式,线程池有哪些
有三种,分别是继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
第一种需要重写父类 Thread 的 run()
方法,并且调用 start()
方法启动线程。
class ThreadTask extends Thread {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
task.start();
}
}
这种方法的缺点是,如果 ThreadTask 已经继承了另外一个类,就不能再继承 Thread 类了,因为 Java 不支持多重继承。
第二种需要重写 Runnable 接口的 run()
方法,并将实现类的对象作为参数传递给 Thread 对象的构造方法,最后调用 start()
方法启动线程。
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
第三种需要重写 Callable 接口的 call()
方法,然后创建 FutureTask 对象,参数为 Callable 实现类的对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,最后调用 start()
方法启动线程。
这种方法的优点是可以获取线程的执行结果。
有哪几种常见的线程池?
主要有四种:
固定大小的线程池 Executors.newFixedThreadPool(int nThreads);
,适合用于任务数量确定,且对线程数有明确要求的场景。例如,IO 密集型任务、数据库连接池等。
缓存线程池 Executors.newCachedThreadPool();
,适用于短时间内任务量波动较大的场景。例如,短时间内有大量的文件处理任务或网络请求。
定时任务线程池 Executors.newScheduledThreadPool(int corePoolSize);
,适用于需要定时执行任务的场景。例如,定时发送邮件、定时备份数据等。
单线程线程池 Executors.newSingleThreadExecutor();
,适用于需要按顺序执行任务的场景。例如,日志记录、文件处理等。
讲讲SPI
SPI 是 Java 的一种扩展机制,用于加载和注册第三方类库,常见于 JDBC、JNDI 等框架。
双亲委派模型会优先让父类加载器加载类,而 SPI 需要动态加载子类加载器中的实现。
根据双亲委派模型,java.sql.Driver
类应该由父加载器加载,但父类加载器无法加载由子类加载器定义的驱动类,如 MySQL 的 com.mysql.cj.jdbc.Driver
。
那么只能使用 SPI 机制通过 META-INF/services
文件指定服务提供者的实现类。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Enumeration<Driver> drivers = ServiceLoader.load(Driver.class, cl).iterator();
DriverManager 使用了线程上下文类加载器来加载 SPI 的实现类,从而允许子类加载器加载具体的 JDBC 驱动。
循环依赖怎么产生的
简单来说就是两个或多个 Bean 相互依赖,比如说 A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。
Spring事务传播行为有哪些
Spring 定义了七种事务传播行为,其中 REQUIRED 是默认的传播行为,表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
比如说在技术派实战项目中,一个用户解锁付费文章的操作,会涉及到创建支付订单、更新订单状态等好几个数据库操作。
这些不同操作的方法就可以放在一个 @Transactional
注解的方法里,它们就自动在同一个事务里了,要么一起成功,要么一起失败。
当然,还有一些特殊情况。比如,我们希望记录一些操作日志,但不想因为主业务失败导致日志回滚。这时候 REQUIRES_NEW 就派上用场了。它不管当前有没有事务,都重新开启一个全新的、独立的事务来执行。这样,日志保存的事务和主业务的事务就互不干扰,即使主业务失败回滚,日志也能妥妥地保存下来。
另外,还有像 SUPPORTS、 NOT_SUPPORTED 这些。SUPPORTS 比较佛系,有事务就用,没事务就不用,适合一些不重要的更新操作。而 NOT_SUPPORTED 则更干脆,它会把当前的事务挂起,以非事务的方式去执行。比如说我们的事务里需要调用一个第三方的、响应很慢的接口,如果这个调用也包含在事务里,就会长时间占用数据库连接。把它用 NOT_SUPPORTED 包起来,就可以避免这个问题。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void callExternalApi() {
// 调用第三方接口
}
最后还有一个比较特殊的 NESTED,嵌套事务。它有点像 REQUIRES_NEW,但又不完全一样。NESTED 是父事务的一个子事务,父事务回滚,它肯定也得回滚。但它自己回滚,却不会影响到父事务。这个特性在处理一些批量操作,希望能部分回滚的场景下特别有用。不过它需要数据库支持 Savepoint 功能,MySQL 就支持。
MySQL的脏读、幻读、不可重复读
比如说在读未提交的隔离级别下,会出现脏读现象:一个事务C 读取了事务B 尚未提交的修改数据。如果事务B 最终回滚,事务C 读取的数据就是无效的“脏数据”。
通过升级隔离级别为读已提交可以解决脏读的问题。
但会出现不可重复读的问题:事务B 第一次读取某行数据值为X,期间事务C修改该数据为Y并提交,事务B 再次读取时发现值变为Y,导致两次读取结果不一致。
可以通过升级隔离级别为可重复读来解决不可重复读的问题。
但可重复读级别下仍然会出现幻读的问题:事务B 第一次查询获得 2条数据,事务C 新增 1条数据并提交后,事务B 再次查询时仍然为 2 条数据,但可以更新新增的数据,再次查询时就发现有 3 条数据了。
MySQL分库分表有做过吗
做过垂直分库:按照业务模块将不同的表拆分到不同的库中,比如说用户、登录、权限等表放在用户库中,商品、分类、库存放在商品库中,优惠券、满减、秒杀放在活动库中。
和水平分表。比如说我们可以将文章表拆分成多个表,如 article_0、article_9999、article_19999 等。
在技术派实战项目中,我们将文章的基本信息和内容详情做了垂直分表处理,因为文章的内容会占用比较大的空间,在只需要查看文章基本信息时把文章详情也带出来的话,就会占用更多的网络 IO 和内存导致查询变慢;而文章的基本信息,如标题、作者、状态等信息占用的空间较小,很适合不需要查询文章详情的场景。
MySQL的存储引擎和区别
MySQL 支持多种存储引擎,常见的有 MyISAM、InnoDB、MEMORY 等。
redis用在什么场景
Redis 可以用来做缓存,比如说把高频访问的文章详情、商品信息、用户信息放入 Redis 当中,并通过设置过期时间来保证数据一致性,这样就可以减轻数据库的访问压力。
redis做缓存要考虑哪些问题,在业务方面呢
一类是经典的缓存系统设计问题(穿透、击穿、雪崩),另一类是与业务逻辑紧密相关的业务缓存问题(数据一致性、缓存粒度等)。
当修改了数据库的数据后,如何保证缓存里的数据也同步更新?如果处理不好,用户就会看到“脏数据”。
另外就是我们应该缓存一个完整的、包含各种关联信息的复杂对象,还是只缓存那些最常用的基础字段?
有用过Redis做消息队列吗
Redis 实现异步消息队列是一个很实用的技术方案,最简单的方式是使用 List 配合 LPUSH 和 RPOP 命令。
有用过其他的消息队列吗
RocketMQ,像 PmHub 中的任务审批,就用了 RocketMQ 来做解耦。
RocketMQ怎么保证消息顺序
RocketMQ 提供了两种级别的顺序消息:全局顺序和局部顺序。
全局顺序是指整个 Topic 的所有消息都严格按照发送顺序消费,这种方式性能比较低,实际项目中用得不多。
局部顺序是指特定分区内的消息保证顺序,这是我们常用的方式。
要保证顺序,关键是要把需要保证顺序的消息发送到同一个 MessageQueue 中。
// 根据订单ID选择队列,保证同一订单的消息在同一队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg;
int index = orderId.hashCode() % mqs.size();
return mqs.get(index);
}
}, orderId);
每个 MessageQueue 在 Broker 中对应一个 ConsumeQueue,消息按照到达 Broker 的顺序依次写入。
当消费者开始消费某个 MessageQueue 时,会在 Broker 端对该队列加锁,其他消费者就无法同时消费这个队列。这样确保了同一时间只有一个消费者在处理某个队列的消息,从而保证了消费顺序。
更多问题
一些重复性的问题大家可以直接通过面渣逆袭在线版查看。
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 9800 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个 简历精修 + 编程项目实战+ Java 面试指南的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远。
欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。
回复