3/30 天崩开局!两个小时!美团一面!

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 3/30 天崩开局!两个小时!美团一面!

下面我将分享一位同学在「美团后端一面的面试经历」,对于这次面试,「他的评价是,很有难度,你试试呢」

【提醒】通过这次面试经验,你将可以复习到以下知识点:

  1. 算法题:LeetCode124,LeetCode23
  2. 集合:String、StringBuilder、StringBuffer的区别,哈希冲突的解决办法,HashMap的底层原理及扩容机制
  3. JUC:共享变量的可见性,volatile的原理,线程池的创建和参数,ReentrantLock的实现及公平性,ThreadLocal的原理及内存泄漏问题
  4. JVM:内存区域划分,GC的触发条件,对象死亡的判断方法
  5. MySQL:主从一致性的保持,分表的方法,Binlog的流程,MySQL的引擎比较,索引的选择原理

「面试官」: 你好,首先我们来做一道算法题,LeetCode124,你解决过这道题吗?

「求职者」: 是的,我记得这道题的解题思路。

「面试官」: 很好,那你能简单描述一下这道题的解题思路吗?

「求职者」: 这道题是求一棵二叉树中的最大路径和,我是采用了深度优先搜索的方法,通过后序遍历的方式,对每一个节点,分别计算包含该节点的最大路径和,并更新全局最大路径和。

「面试官」: 明白了。那你能写出解决这道题的代码吗?

「求职者」: 当然,这里是我写的代码:

class Solution {
    int maxSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        maxGain(root);
 return maxSum;  }   private int maxGain(TreeNode node) {  if (node == null) {  return 0;  }   int leftGain = Math.max(maxGain(node.left), 0);  int rightGain = Math.max(maxGain(node.right), 0);   int priceNewpath = node.val + leftGain + rightGain;   maxSum = Math.max(maxSum, priceNewpath);   return node.val + Math.max(leftGain, rightGain);  } } 

img

「面试官」: 很好。现在我们来讨论一下集合,你能告诉我String、StringBuilder和StringBuffer的区别吗?

「求职者」: 当然可以。String,StringBuilder和StringBuffer三者在Java中都用来存储和操作字符串,但是他们在性能和使用场景上有所不同。首先,String是不可变的,也就是说一旦一个String对象被创建,我们不能改变它的内容。这是因为String的实现中,值被存储在一个final的字符数组中。而StringBuilder和StringBuffer是可变的,他们可以添加,删除,插入等操作来改变其内容。他们的值被存储在一个字符数组中,当数组容量不足时,会自动扩容。最主要的区别在于,StringBuilder是线程不安全的,而StringBuffer是线程安全的,因为StringBuffer的大部分方法都是synchronized的。


img

「面试官」: 那么,你能解释一下哈希冲突的解决办法吗?

「求职者」: 哈希冲突是指当两个不同的键的哈希值相同时,会发生的情况。解决哈希冲突的常见办法有两种,一种是拉链法,另一种是开放寻址法。拉链法是将哈希到同一位置的元素链接在一起,形成一个链表。当发生冲突时,就在链表的末尾添加元素。而开放寻址法则是当哈希位置已经被占用时,通过探测其他位置来找到一个未被占用的位置。

面试官」: 明白了。那你能详细介绍一下HashMap的底层原理吗?

「求职者」: 当然。HashMap是Java中最常用的哈希表实现。它由一个Entry数组和一些链表组成。每个Entry存储了键值对。当我们向HashMap中插入一个键值对时,首先会计算键的哈希值,然后使用这个哈希值来决定该键值对应该存储在数组的哪个位置。如果该位置已经有其他键值对,那么就在链表的末尾添加新的键值对。至于扩容,当HashMap中的键值对数量超过数组长度和加载因子的乘积时,就会对数组进行扩容。扩容后的数组长度是原数组长度的两倍。扩容操作会重新计算每个键的哈希值,并将键值对放在新的位置上。

「面试官」: 很好。那么,如果有100个数据,怎么确定hashmap的容量,使得它不用扩容?

「求职者」: 确定HashMap的容量需要考虑到负载因子。负载因子是一个浮点数,其值表示了当HashMap被填充的程度。默认的负载因子是0.75。所以,如果有100个数据,为了使得HashMap不用扩容,我们需要先确定一个初步的容量,就是100除以负载因子,也就是100/0.75=133。但是,HashMap的容量总是2的幂次方,所以我们需要选择一个大于等于133的最小的2的幂次方,也就是256。所以,对于这个问题,我们应该将HashMap的初始容量设为256。

「面试官」: 接下来我们来讨论一下Java并发包,你是否了解如何保证共享变量的可见性?

「求职者」: 是的,Java中通过使用volatile关键字来保证共享变量的可见性。当一个共享变量被volatile修饰后,它会保证每次读变量的时候都从内存中读,跳过CPU缓存这一步。同时每次写变量的时候,都会立即将变量的新值刷新到主内存中,这样就能保证在多线程环境下,所有线程都能看到共享变量的最新值。

「面试官」: 那么,你能解释一下volatile的原理吗?

「求职者」: volatile关键字的原理主要是通过内存屏障和禁止指令重排序来实现的。内存屏障是一种处理器指令,它能确保特定操作的执行顺序,并且能够保证某些变量的内存可见性。通过插入内存屏障,我们可以禁止特定类型的处理器重排序。对于volatile修饰的变量,编译器和运行时都会插入内存屏障来禁止指令重排序,确保执行顺序符合代码的书写顺序。

「面试官」: 很好。你是否使用过线程池?能否讲一下它的工作原理?

求职者」: 线程池主要是为了减少在创建和销毁线程上所花的时间以及系统资源的消耗,达到重用已创建的线程的目的。在Java中,我们可以通过Executor框架在Java.util.concurrent包中的ThreadPoolExecutor类来创建线程池。线程池主要包含一个线程池和一个任务队列。当新任务来临时,首先会尝试将其添加到队列中。如果队列已满,那么会尝试创建新的线程处理任务。如果线程数量达到线程池的最大值,那么会根据饱和策略来处理无法处理的任务。

「面试官」: 你能详细介绍一下线程池的参数配置吗?

「求职者」: 当然。线程池主要有以下几个参数:

  1. corePoolSize:线程池的核心线程数量。
  1. maximumPoolSize:线程池的最大线程数量。
  2. keepAliveTime:非核心线程的空闲存活时间。
  3. TimeUnit:keepAliveTime的时间单位。
  4. BlockingQueue:任务队列。
  5. ThreadFactory:线程工厂,用于创建新的线程。
  6. RejectedExecutionHandler:饱和策略,当队列和最大线程池都满了之后,如何处理新来的任务。

「面试官」: 了解了。那你是否了解ReentrantLock,它是如何实现可重入的?

「求职者」: ReentrantLock是一种可重入的互斥锁,也就是说,一个线程可以多次获取同一个锁。在ReentrantLock中,有一个状态变量state,当一个线程首次获取锁时,会将state设置为1。如果这个线程再次获取锁,那么会将state加1。每次释放锁,都会将state减1。只有当state变为0时,锁才真正被释放。

「面试官」: 那么,ReentrantLock是公平锁吗?

求职者」: ReentrantLock可以是公平锁,也可以是非公平锁,这取决于我们在创建ReentrantLock时传入的参数。如果我们在创建ReentrantLock时传入的参数为true,那么它就是一个公平锁。在公平锁模式下,线程会按照请求锁的顺序获得锁。如果我们在创建ReentrantLock时传入的参数为false或者不传入参数,那么它就是一个非公平锁。在非公平锁模式下,当一个线程请求锁时,如果锁当前没有被其他线程持有,那么这个线程会直接获得锁,不管是否有其他线程正在等待这个锁。

面试官」: 你是否使用过ThreadLocal,它的原理是什么?

「求职者」: 是的,我使用过ThreadLocal。ThreadLocal是Java中用来实现线程局部变量的类,也就是说,每个线程都可以有一个自己的局部变量,而这个局部变量对其他线程是不可见的。ThreadLocal的实现原理是,每个Thread维护一个ThreadLocalMap,这个Map的键是ThreadLocal对象,值是真正的局部变量。当我们通过ThreadLocal的get或set方法访问局部变量时,实际上是通过当前线程的ThreadLocalMap来存取的。

「面试官」: 那么,ThreadLocal是否可能引起内存泄漏,如果会,应该如何避免?

「求职者」: 是的,ThreadLocal可能会引起内存泄漏。这是因为,ThreadLocalMap的生命周期和Thread是一样的,如果ThreadLocal没有被外部强引用持有,那么这个ThreadLocal是可以被垃圾回收器回收的,但是如果我们忘记了从ThreadLocalMap中移除对应的entry,那么即使ThreadLocal对象已经被回收,由于Thread一直在运行,它的ThreadLocalMap及其entry的值(也就是我们的局部变量)会一直占用内存,这就会导致内存泄漏。为了避免这种情况,我们需要在不再使用局部变量时,显式地调用ThreadLocal的remove方法来移除当前线程ThreadLocalMap中的entry。

「面试官」: 很好,这部分你回答得很详尽。现在让我们转到JVM,你提到了内存区域划分,能详细说明一下吗?

「求职者」: 当然可以。在JVM中,内存主要被分为几个区域:堆(Heap),方法区(Method Area),程序计数器(Program Counter Register),虚拟机栈(JVM Stack),和本地方法栈(Native Method Stack)。堆是用来存储对象实例和数组的地方,是垃圾收集器进行垃圾回收的主要区域。方法区用来存储已被虚拟机加载的类信息、常量、静态变量等数据。程序计数器存储当前线程所执行的字节码的行号指示器。虚拟机栈存储局部变量表、操作数栈、动态链接、方法出入口等信息。本地方法栈则服务于本地方法调用。

「面试官」: 你提到了GC,GC是在什么时候触发的?

「求职者」: GC主要在两个场景下触发:一是当堆内存不足时,JVM会触发GC来回收空间,二是System.gc()方法被调用时,JVM同样会尝试进行垃圾回收,尽管调用System.gc()并不保证GC一定会执行。GC会根据不同的垃圾收集器和堆的区域(如Young Generation和Old Generation)以不同的方式进行。

「面试官」: 那么,JVM是如何判断一个对象是否已经死亡?

「求职者」: JVM主要通过可达性分析来判断对象是否存活。在可达性分析中,一系列的称为"GC Roots"的对象被选定,然后JVM从这些节点开始向下搜索,如果一个对象到GC Roots没有任何引用链相连(即不可达),那么这个对象就被认为是死亡的,可以被垃圾回收器回收。除此之外,也有其他的方法,例如引用计数法,但是由于它不能解决对象之间的循环引用问题,所以在Java的垃圾回收器中并不采用。

「面试官」: 明白了。你对MySQL、Redis和Spring哪个更熟悉?

「求职者」: 我对MySQL和Redis比较熟悉,对Spring也有一定的了解。

「面试官」: 好的,让我们继续深入MySQL。你能告诉我MySQL主从一致性是如何保持的吗?

「求职者」: MySQL主从一致性主要是通过binlog(二进制日志)来保持的。在主服务器上,所有的更改(比如INSERT、UPDATE和DELETE操作)都会记录在binlog中。然后,从服务器会从主服务器上读取这些binlog,并在自己上面重放这些操作,以此来保证数据的一致性。

「面试官」: 那么,如果有大量数据,你会如何进行分表?

「求职者」: 当单一表中的数据量过大时,会对数据库性能产生影响。此时,我们可以采用分表的策略,即将一个大表分成多个小表来管理。分表可以根据不同的需求采取不同的策略,比如根据时间范围进行分表,这种情况下可以进行冷热数据分离;或者根据某个字段的值进行水平分表,可以使用一致性哈希等算法来分配数据到不同的表中,以保持负载均衡。

「面试官」: 很好,那你对binlog的具体流程了解吗?

「求职者」: binlog的具体流程主要分为几个步骤。首先,在主服务器上执行的数据变更操作会被记录到binlog中。然后,从服务器上的IO线程会连接到主服务器,并请求从上次断开的位置开始复制binlog。复制到从服务器的binlog会被写入到中继日志(relay log)中。接着,从服务器的SQL线程会读取中继日志,并执行其中的操作来更新从服务器上的数据。

「面试官」: 明白了。你能比较一下MySQL不同的存储引擎吗?例如InnoDB和MyISAM。

求职者」: 当然可以。InnoDB和MyISAM是MySQL中两种常见的存储引擎。InnoDB支持事务处理,具有ACID(原子性、一致性、隔离性、持久性)特性,支持外键,适合处理大量的短小的、需要频繁更新的查询。而MyISAM不支持事务处理,也不支持外键,但是在查询性能上通常比InnoDB要快,适合读取操作远多于写入操作的场景。另外,InnoDB支持行级锁定,而MyISAM只支持表级锁定,这使得InnoDB在并发操作上有更好的性能。

「面试官」: 最后一个问题,为什么MySQL的索引通常使用B+树而不是其他数据结构,比如二叉搜索树或红黑树?

「求职者」: B+树作为MySQL索引的数据结构,它在磁盘读写方面有优势。二叉搜索树虽然在理论上查找效率是对数级别的,但是由于树的深度可能会很大,在实际的磁盘IO操作中会导致大量的随机读取,效率不高。而红黑树作为一种平衡二叉搜索树,虽然可以保证最坏情况下的效率,但是它的填充因子低,也就是说,树的节点没有充分利用磁盘的页。相比之下,B+树有较高的填充因子,可以减少IO次数,并且B+树的所有值都存在叶子节点,并且叶子节点之间是相互链接的,这对于范围查询非常有效。

「面试官」: 好了,今天的面试结束,回去等通知吧。

本文使用 mdnice 排版


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
Kubernetes 容器
要获取ACK(阿里云容器服务)集群中的Deployment
要获取ACK(阿里云容器服务)集群中的Deployment【1月更文挑战第8天】【1月更文挑战第40篇】
187 4
|
JSON 缓存 前端开发
阿里开发手册 嵩山版-编程规约 (十) 前后端规约
《阿里开发手册 嵩山版》中关于前后端规约的部分,涵盖了前后端交互的API设计、数据格式、错误处理、安全性等关键编程规约,目的是确保前后端开发高效协同,提升软件交付质量。
 阿里开发手册 嵩山版-编程规约 (十) 前后端规约
|
11月前
|
Java 微服务 Spring
springBoot:@Enable&@import&事件监听 (六)
本文介绍了如何在Spring Boot项目中实现跨项目获取Bean类的方法,包括创建两个项目、在pom文件中导入依赖、定义Bean和配置类、封装注解@EnableStudent以简化导入过程。同时,详细讲解了@Import的四种用法,并展示了如何通过实现ApplicationRunner、CommandLineRunner接口及配置spring.factories文件来执行程序启动时的监听逻辑。
133 2
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
344 0
liteflow快速开始
liteflow快速开始
189 0
|
JavaScript
(详解)Vue3自定义指令
(详解)Vue3自定义指令
393 2
|
SQL 开发框架 编解码
前端开发规范
前端开发规范
1332 0
前端开发规范
|
测试技术 API 区块链
Hyperledger fabric部署链码(三)批准链码定义
fabric部署chaincode-go(智能合约)系列之三
236 0
|
消息中间件 网络协议 Ubuntu
【内网穿透】远程访问RabbitMQ服务
RabbitMQ是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message,下面介绍通过在ubuntu+cpolar+rabbitMQ环境下,实现mq服务端远程访问。
word长公式不换行显示的方法
word长公式不换行显示的方法