金山还是小米,谁才是雷军的亲儿子?附小米线下一面面经(八股盛宴)

大家好,我是二哥呀。

上次发小米的帖子里,有同学留言说:"武汉金山本科都能开到20k以上,谁是雷总亲儿子不用我多说了吧?"

你别说,真别说,金山如果真能开到 20k 以上,还是本科,那绝逼是比小米亲儿子的。

但是金山招人太少了,并且主要是 Go 岗居多。想冲 Go 的同学可以参考一下我这个学习路线,很顶的,Google 搜 Go 学习路线排名第一。

等后续校招派、简历派这两个项目做完后,我也打算做一些 Go 的实战项目,计划先把派聪明开个 Go 的分支,再做几个轮子,有需要的同学可以 push 一下我。

就目前来说,仍然推荐大家多冲冲小米,是不是亲儿子不知道,反正岗位是比金山要多不少。

刚好最近也有球友参加了小米的线下面试,还分享了面经,就借这个机会,给大家复盘下这些面试题,方便后续参加面试的同学做个参考,原题出现的概率很大。

因为这些题目都很高频,主要集中在 Java 基础、MySQL、Spring、Redis、消息队列上面,大家在面试的时候也一定要注意每家公司的特点,好针对性地去复习准备。

同学 P小米线下面试

自我介绍

我是来自家里蹲大学的小王,之前参加过小米的训练营。项目目前做一个基于 RAG 架构的企业级私有知识库派聪明,其核心意义在于解决现代企业知识管理的痛点,推动组织智能化转型。

派聪明通过集成 Apache Tika 文档解析、DeepSeek 大模型向量化和 Elasticsearch 混合检索技术,构建了一套完整的智能知识处理流水线。系统能够自动解析 Word、PDF、Excel 等多种格式文档,将非结构化信息转化为可检索的知识资产。更重要的是,基于语义理解的向量检索技术突破了传统关键词匹配的局限,用户可以用自然语言描述需求,系统能够理解意图并返回相关内容。

另外一个项目是 xxxx。

我个人也是小米的忠实用户,家里的智能设备都买的清一色小米,手机也一直用的小米,也非常期待能加入到小米这个大家庭。

concurrentHashMap的底层原理

ConcurrentHashMap 是 HashMap 的线程安全版本。

JDK 7 采用的是分段锁,整个 Map 会被分为若干段,每个段都可以独立加锁。不同的线程可以同时操作不同的段,从而实现并发。

JDK 8 使用了一种更加细粒度的锁------桶锁,再配合 CAS + synchronized 代码块控制并发写入,以最大程度减少锁的竞争。

对于读操作,ConcurrentHashMap 使用了 volatile 变量来保证内存可见性。

对于写操作,ConcurrentHashMap 优先使用 CAS 尝试插入,如果成功就直接返回;否则使用 synchronized 代码块进行加锁处理。

ThreadLocal底层原理

当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量,这样就相当于为每个线程维护了一个变量副本。

Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry.value 设置为 null,这样可以在很大程度上避免内存泄漏。

简版回答:

ThreadLocal 的实现原理是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。

1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。

2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。

3、Map 的大小由 ThreadLocal 对象的多少决定。

讲讲自己熟悉的一个垃圾回收器,回收思想和具体工作流程

我比较熟悉的是 G1,它在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。

G1 把 Java 堆划分为多个大小相等的独立区域 Region,每个区域都可以扮演新生代或老年代的角色。

同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。

大对象的判定规则是,如果一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中。

这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。

G1 收集器的运行过程大致可划分为这几个步骤:

①、并发标记,G1 通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。

②、混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。

选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。

③、可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

聚簇索引、覆盖索引?

聚簇索引的叶子节点存储了完整的数据行,数据和索引是在一起的。InnoDB 的主键索引就是聚簇索引,叶子节点不仅存储了主键值,还存储了其他列的值,因此按照主键进行查询的速度会非常快。

每个表只能有一个聚簇索引,通常由主键定义。如果没有显式指定主键,InnoDB 会隐式创建一个隐藏的主键索引 row_id。

覆盖索引指的是:查询所需的字段全部都在索引中,不需要回表,从索引页就能直接返回结果。

empname 和 job 两个字段是一个联合索引,而查询也恰好是这两个字段,这时候单次查询就可以达到目的,不需要回表。

可以将高频查询的字段(如 WHERE 条件和 SELECT 列)组合为联合索引,实现覆盖索引。

例如:

sql 复制代码
CREATE INDEX idx_empname_job ON employee(empname, job);

这样查询的时候就可以走索引:

sql 复制代码
SELECT empname, job FROM employee WHERE empname = '王二' AND job = '程序员';

普通索引只用于加速查询条件的匹配,而覆盖索引还能直接提供查询结果。

mysql的MVCC和两阶段提交怎么实现的

MVCC 指的是多版本并发控制,每次修改数据时,都会生成一个新的版本,而不是直接在原有数据上进行修改。并且每个事务只能看到在它开始之前已经提交的数据版本。

这样的话,读操作就不会阻塞写操作,写操作也不会阻塞读操作,从而避免加锁带来的性能损耗。

其底层实现主要依赖于 Undo Log 和 Read View。

每次修改数据前,先将记录拷贝到Undo Log,并且每条记录会包含三个隐藏列,DB_TRX_ID 用来记录修改该行的事务 ID,DB_ROLL_PTR 用来指向 Undo Log 中的前一个版本,DB_ROW_ID 用来唯一标识该行数据(仅无主键时生成)。

每次读取数据时,都会生成一个 ReadView,其中记录了当前活跃事务的 ID 集合、最小事务 ID、最大事务 ID 等信息,通过与 DB_TRX_ID 进行对比,判断当前事务是否可以看到该数据版本。

为保证两种日志的一致性,innodb 采用了两阶段提交策略,redo log 在事务执行过程中持续写入,并在事务提交前进入 prepare 状态;binlog 在事务提交的最后阶段写入,之后 redo log 会被标记为 commit 状态。

可以通过回放 binlog 实现数据同步或者恢复到指定时间点;redo log 用来确保事务提交后即使系统宕机,数据仍然可以通过重放 redo log 恢复。

java中的字节输入流和字节输出流用了什么设计模式?你对这种设计模式如何理解?

用到了------装饰器模式,装饰器模式的核心思想是在不改变原有对象结构的前提下,动态地给对象添加新的功能。

具体到 Java IO 中,InputStream 和 OutputStream 这些抽象类定义了基本的读写操作,然后通过各种装饰器类来增强功能。比如 BufferedInputStream 给基础的输入流增加了缓冲功能,DataInputStream 增加了读取基本数据类型的能力,它们都是对基础流的装饰和增强。

java 复制代码
InputStream input = new BufferedInputStream(
    new DataInputStream(
        new FileInputStream("data.txt")
    )
);

这里 FileInputStream 提供基本的文件读取能力,DataInputStream 装饰它增加了数据类型转换功能,BufferedInputStream 再装饰它增加了缓冲功能。每一层装饰都在原有功能基础上增加新特性,而且可以灵活组合。

我对装饰器模式的理解是它很好地体现了"组合优于继承"的设计原则。优势在于运行时动态组合功能,而且遵循开闭原则,可以在不修改现有代码的情况下增加新功能。

讲讲spring的Bean的生命周期

Bean 的生命周期可以分为 5 个主要阶段,我按照实际的执行顺序来说一下。

第一个阶段是实例化。Spring 容器会根据 BeanDefinition,通过反射调用 Bean 的构造方法创建对象实例。如果有多个构造方法,Spring 会根据依赖注入的规则选择合适的构造方法。

第二阶段是属性赋值。这个阶段 Spring 会给 Bean 的属性赋值,包括通过 @Autowired@Resource 这些注解注入的依赖对象,以及通过 @Value 注入的配置值。

第三阶段是初始化。这个阶段会依次执行:

  • @PostConstruct 标注的方法
  • InitializingBean 接口的 afterPropertiesSet 方法
  • 通过 @Bean 的 initMethod 指定的初始化方法

我在项目中经常用 @PostConstruct 来做一些初始化工作,比如缓存预加载、DB 配置等等。

java 复制代码
// CategoryServiceImpl中的缓存初始化
@PostConstruct
public void init() {
    categoryCaches = CacheBuilder.newBuilder().maximumSize(300).build(new CacheLoader<Long, CategoryDTO>() {
        @Override
        public CategoryDTO load(@NotNull Long categoryId) throws Exception {
            CategoryDO category = categoryDao.getById(categoryId);
            // ...
        }
    });
}

// DynamicConfigContainer中的配置初始化
@PostConstruct
public void init() {
    cache = Maps.newHashMap();
    bindBeansFromLocalCache("dbConfig", cache);
}

初始化后,Spring 还会调用所有注册的 BeanPostProcessor 后置处理方法。这个阶段经常用来创建代理对象,比如 AOP 代理。

第五阶段是使用 Bean。比如我们的 Controller 调用 Service,Service 调用 DAO。

java 复制代码
// UserController中的使用示例
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
    return userService.getUserById(id);
}
// UserService中的使用示例
@Autowired
private UserDao userDao;
public UserDTO getUserById(Long id) {
    return userDao.getById(id);
}
// UserDao中的使用示例
@Autowired
private JdbcTemplate jdbcTemplate;
public UserDTO getById(Long id) {
    String sql = "SELECT * FROM users WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
}

最后是销毁阶段。当容器关闭或者 Bean 被移除的时候,会依次执行:

  • @PreDestroy 标注的方法
  • DisposableBean 接口的 destroy 方法
  • 通过 @Bean 的 destroyMethod 指定的销毁方法

项目中为什么用两种消息队列?(rabbitMq和Kafka)出于什么样的考虑?

在技术派项目中,我们根据业务特点选择了不同的消息队列。RabbitMQ 主要用于处理用户互动相关的实时消息,比如点赞、评论、收藏等操作;用 Kafka 主要是因为很多企业级的项目会用到,所以我就想提前了解和使用,和企业的真实开发接轨。

布隆过滤器的底层数据结构是什么?主要用来解决什么问题?

布隆过滤器是一种空间效率极高的概率性数据结构,用于快速判断一个元素是否在一个集合中。它的特点是能够以极小的内存消耗,判断一个元素"一定不在集合中"或"可能在集合中",常用来解决 Redis 缓存穿透的问题。

布隆过滤器的核心由一个很长的二进制向量和一系列哈希函数组成。

  • 初始化的时候,创建一个长度为 m 的位数组,初始值全为 0,同时选择 k 个不同的哈希函数
  • 当添加一个元素时,用 k 个哈希函数计算出 k 个哈希值,然后对 m 取模,得到 k 个位置,将这些位置的二进制位都设为 1
  • 当需要判断一个元素是否在集合中时,同样用 k 个哈希函数计算出 k 个位置,如果这些位置的二进制位有任何一个为 0,该元素一定不在集合中;如果全部为 1,则该元素可能在集合中
相关推荐
it_czz几秒前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者3 分钟前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下1 小时前
apifox登录后设置token到环境变量
java·后端
程序员小寒1 小时前
从一道前端面试题,谈 JS 对象存储特点和运算符执行顺序
开发语言·前端·javascript·面试
hashiqimiya2 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev2 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG2 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范
用户8356290780513 小时前
用Python轻松管理Word页脚:批量处理与多节文档技巧
后端·python
青莲8433 小时前
Android 事件分发机制 - 事件流向详解
android·前端·面试
想用offer打牌3 小时前
一站式了解Spring AI Alibaba的流式输出
java·人工智能·后端