2025最新 Java 面经:美团后端面试真实复盘,附答案模板,速速收藏!

1. 线程状态有哪些?

在 Java 中,线程的生命周期可以细化为以下几个状态:

  • New(初始状态):线程对象创建后,但未调用 start() 方法。
  • Runnable(可运行状态):调用 start() 方法后,线程进入就绪状态,等待 CPU 调度。
  • Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。
  • Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。
  • Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
  • Terminated(终止状态):线程执行完成或因异常退出。

2. wait 和 sleep 区别

wait()sleep() 都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):

1)使用要求不同

  • wait() 方法必须在同步块或同步方法内调用 ,否则会抛出 IllegalMonitorStateException。这是因为 wait() 依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。
  • sleep() 方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程会进入休眠状态,但不会释放它持有的任何锁。

2)方法所属类不同

  • wait() :属于 Object 类。
  • sleep() :属于 Thread 类。

3)恢复方式不同

  • wait() :需要被其他线程通过 notify()notifyAll() 显式唤醒,或被 wait(long timeout) 的超时参数唤醒。
  • sleep() :在指定时间后自动恢复运行,或通过抛出 InterruptedException 恢复。

4)用途不同

  • wait() :通常用于线程间通信,配合 notify()notifyAll() 来实现线程的协调工作。
  • sleep() :用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。

3. 线程池有哪些参数?一般如何设置

线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。

它几个关键的配置包括:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。

主要工作原理如下:

  1. 默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。
  2. 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
  3. 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
  4. 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
  5. 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。

Java 并发库中提供了 5 种常见的线程池实现 ,主要通过 Executors 工具类来创建。

1)FixedThreadPool:创建一个固定数量的线程池。

线程池中的线程数是固定的,空闲的线程会被复用。如果所有线程都在忙,则新任务会放入队列中等待。

适合负载稳定的场景,任务数量确定且不需要动态调整线程数。

2)CachedThreadPool:一个可以根据需要创建新线程的线程池。

线程池的线程数量没有上限,空闲线程会在 60 秒后被回收,如果有新任务且没有可用线程,会创建新线程。

适合短期大量并发任务的场景,任务执行时间短且线程数需求变化较大。

3)SingleThreadExecutor:创建一个只有单个线程的线程池。

只有一个线程处理任务,任务会按照提交顺序依次执行。

适用于需要保证任务按顺序执行的场景,或者不需要并发处理任务的情况。

4)ScheduledThreadPool:支持定时任务和周期性任务的线程池。

可以定时或以固定频率执行任务,线程池大小可以由用户指定。

适用于需要周期性任务执行的场景,如定时任务调度器。

5)WorkStealingPool:基于任务窃取算法的线程池。

线程池中的每个线程维护一个双端队列(deque),线程可以从自己的队列中取任务执行。如果线程的任务队列为空,它可以从其他线程的队列中"窃取"任务来执行,达到负载均衡的效果。

适合大量小任务并行执行,特别是递归算法或大任务分解成小任务的场景。

线程池的线程数设置需要看具体执行的任务是什么类型的。

任务类型可以分:CPU 密集型任务和 I/O 密集型任务。

CPU 密集型任务

CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。

根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1

I/O 密集型任务

I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。

根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2 或更多一些。

(这句话一定要和面试官说)以上公式仅是一个纯理论值,仅供参考!在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。

4. Error 和 Exception 有什么区别

ExceptionError 都是 Throwable 类的子类(在 Java 代码中只有继承了 Throwable 类的实例才可以被 throw 或者被 catch)它们表示在程序运行时发生的异常或错误情况。

总结来看:Exception 表示可以被处理的程序异常,Error 表示系统级的不可恢复错误。

详细说明

1)Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。

常见子类有:IOExceptionSQLExceptionNullPointerExceptionIndexOutOfBoundsException 等。

Exception 又分为 Checked Exception (编译期异常)和 Unchecked Exception(运行时异常)。

  • Checked Exception :在编译时必须显式处理(如使用 try-catch 块或通过 throws 声明抛出)。如 IOException
  • Unchecked Exception :运行时异常,不需要显式捕获。常见的如 NullPointerExceptionIllegalArgumentException 等,继承自 RuntimeException

2)Error :表示严重的错误,通常是 JVM 层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError)、栈溢出(StackOverflowError)。

Error 不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。

5.Synchronized 和 ReentrantLock 有什么区别?

Synchronized 是 Java 内置的关键字,实现基本的同步机制,不支持超时,非公平,不可中断,不支持多条件。

ReentrantLock 是 JUC 类库提供的,由 JDK 1.5 引入,支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持多条件判断。

ReentrantLock 需要手动解锁,而 Synchronized 不需要,它们都是可重入锁。

一般情况下用 Synchronized 足矣,比较简单,而 ReentrantLock 比较灵活,支持的功能比较多,所以复杂的情况用 ReentrantLock 。

6. 说下 AQS 原理

简单来说 AQS 就是起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他相关 JUC 锁的使用,具体加锁时机、入队时机等都需要实现类自己控制。

它主要通过维护一个共享状态(state)和一个先进先出(FIFO)的等待队列,来管理线程对共享资源的访问。

state 用 volatile 修饰,表示当前资源的状态。例如,在独占锁中,state 为 0 表示未被占用,为 1 表示已被占用。

当线程尝试获取资源失败时,会被加入到 AQS 的等待队列中。这个队列是一个变体的 CLH 队列,采用双向链表结构,节点包含线程的引用、等待状态以及前驱和后继节点的指针。

AQS 常见的实现类有 ReentrantLock、CountDownLatch、Semaphore 等等。

7. 分布式锁

分布式锁需要实现多个应用实例之间的临界资源竞争,因此它需要依赖三方组件才能实现这样的功能。

常见依赖 Redis、ZooKeeper 来实现分布式锁。

8. 发现数据库查询很慢,如何排查

平时进行 SQL 调优,主要是通过观察慢 SQL,然后利用 explain 分析查询语句的执行计划,识别性能瓶颈,优化查询语句。

9. 索引数据结构 b+树 ,什么时候索引无效?

一般有以下几种情况不推荐建立索引:

1)对于数据量很小的表

2)频繁更新的表

3)执行大量的 SELECT

4)低选择性字段(高度重复值的列)

5)低频查询的列

6)长文本字段

10. CAS 在高并发的情况下会有问题吗

CAS 是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。

工作原理

  • 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
  • 交换(Swap):如果相等,则将内存中的值更新为新值。
  • 失败重试:如果不相等,说明有其他线程已经修改了该值,CAS 操作失败,一般会利用重试,直到成功。

11. 乐观锁和悲观锁有什么区别?使用场景是?

悲观锁(Pessimistic Locking)

  • 假设会发生冲突,因此在操作数据之前就对数据加锁,确保其他事务无法访问该数据。常见于对数据一致性要求较高的场景。
  • 实现方式:使用行级锁或表级锁,例如可以使用 SELECT ... FOR UPDATELOCK IN SHARE MODE 语句来加锁。

乐观锁(Optimistic Locking)

  • 假设不会发生冲突,因此在操作数据时不加锁,而是在更新数据时进行版本控制或校验。如果发现数据被其他事务修改,则会拒绝当前事务的修改,需重新尝试。
  • 实现方式:通常通过版本号或时间戳来实现,每次更新时检查版本号或时间戳是否一致。

12. 数据库分库分表

分库分表是数据库性能优化的一种方法,通过将数据分散存储在多个数据库或表中,来提高系统的可扩展性、性能和可用性。

如果还想进一步了解 为什么需要分库分表,可以浏览面试鸭,获取扩展知识:美团后端一面

13. 接口耗时较高,怎么排查?(除了业务优化)

看服务器相关监控

  • CPU 和内存使用率:检查服务器的 CPU 和内存使用率是否过高,导致接口响应变慢。可以使用 top、htop 或者 vmstat 等工具进行监控。
  • 磁盘 I/O:如果接口依赖于磁盘操作,如数据库查询或文件读写,磁盘 I/O 的瓶颈会影响接口性能。可以使用 iostat 或者 iotop 工具检查磁盘 I/O 情况。
  • 数据库查询性能:检查数据库查询是否存在慢SQL或索引未命中情况。可以通过数据库自带的慢查询日志或 EXPLAIN 语句分析 SQL 语句的执行计划。
  • 线程池监控:如果接口使用线程池处理请求,线程池配置不合理(如线程数过少或队列过长)可能导致请求等待时间过长。需要检查线程池的大小和队列情况,并根据负载适当调整。
  • 连接池监控:如果使用了连接池(数据库连接池、HTTP 连接池等),需要检查连接池的配置是否合理。连接池大小过小可能导致连接等待时间过长,大小过大则可能耗尽资源。

日志追踪

  • 分布式追踪:使用分布式追踪工具(如 Zipkin、Jaeger)来跟踪接口调用的各个阶段,找出耗时较长的环节。
  • 日志分析:启用详细的日志记录,包括请求接收时间、处理时间、外部依赖(如数据库、外部服务)调用时间。通过分析日志,确定耗时主要集中在哪个阶段。
  • APM 工具:使用应用性能监控(APM)工具(如 New Relic、AppDynamics、Prometheus)来监控接口的性能指标,实时分析性能瓶颈。

14. Object 里都有什么?

以下是 Object 类中的主要方法及其作用:

1. public boolean equals(Object obj)

  • 作用:用于比较两个对象是否相等。默认实现比较对象的内存地址,即判断两个引用是否指向同一个对象。
  • 使用:通常会重写此方法来比较对象的内容或特定属性,以定义对象的相等性。

2. public int hashCode()

  • 作用 :返回对象的哈希码,是对象的整数表示。哈希码用于支持基于哈希的集合(如 HashMapHashSet)。
  • 使用 :如果重写了 equals 方法,则通常也需要重写 hashCode 方法,以保证相等的对象具有相同的哈希码。

3. public String toString()

  • 作用:返回对象的字符串表示。默认实现返回对象的类名加上其哈希码的十六进制表示。
  • 使用:通常会重写此方法以提供对象的更有意义的描述。

4. public final Class<?> getClass()

  • 作用 :返回对象的运行时类(Class 对象)。此方法是 Object 类中的一个 final 方法,不能被重写。
  • 使用:可以用来获取对象的类信息,常用于反射操作。

5. public void notify()

  • 作用:唤醒在对象的监视器上等待的一个线程。该方法需要在同步块或同步方法中调用。
  • 使用:用于在多线程环境中进行线程间的通信和协调。

6. public void notifyAll()

  • 作用:唤醒在对象的监视器上等待的所有线程。该方法需要在同步块或同步方法中调用。
  • 使用 :与 notify() 相似,但唤醒所有等待线程,用于处理多个线程之间的协作。

7. public void wait()

  • 作用 :使当前线程等待,直到其他线程调用 notify()notifyAll() 方法。此方法需要在同步块或同步方法中调用。
  • 使用:用于线程间的通信,线程会等待直到被唤醒或超时。

8. public void wait(long timeout)

  • 作用:使当前线程等待,直到指定的时间到期或被唤醒。超时后线程会自动被唤醒。
  • 使用:用于实现带有超时的等待机制。

9. public void wait(long timeout, int nanos)

  • 作用:使当前线程等待,直到指定的时间和纳秒数到期或被唤醒。
  • 使用:用于实现更精细的等待控制,允许指定等待时间的精确到纳秒。

10. protected Object clone()

  • 作用:创建并返回当前对象的一个副本。默认实现是进行浅拷贝。
  • 使用:通常会重写此方法来实现深拷贝,以确保克隆对象的完整性。

11. protected void finalize()

  • 作用:在垃圾回收器确定不存在对该对象的更多引用时调用,用于进行资源释放等清理工作。
  • 使用 :不建议使用,因为它依赖于垃圾回收器的实现,可能会导致不确定的性能问题。推荐使用 try-with-resourcesAutoCloseable 接口进行资源管理。

更多面经及答案可以打开面试鸭阅读学习 ➡️ :www.mianshiya.com/

相关推荐
左灯右行的爱情1 分钟前
缓存并发更新的挑战
jvm·数据库·redis·后端·缓存
浩宇软件开发5 分钟前
Android开发,实现一个简约又好看的登录页
android·java·android studio·android开发
brzhang5 分钟前
告别『上线裸奔』!一文带你配齐生产级 Web 应用的 10 大核心组件
前端·后端·架构
shepherd1116 分钟前
Kafka生产环境实战经验深度总结,让你少走弯路
后端·面试·kafka
南客先生12 分钟前
多级缓存架构设计与实践经验
java·面试·多级缓存·缓存架构
anqi2714 分钟前
如何在 IntelliJ IDEA 中编写 Speak 程序
java·大数据·开发语言·spark·intellij-idea
袋鱼不重19 分钟前
Cursor 最简易上手体验:谷歌浏览器插件开发3s搞定!
前端·后端·cursor
m0_7401546721 分钟前
maven相关概念深入介绍
java·maven
zayyo21 分钟前
Vue.js性能优化新思路:轻量级SSR方案深度解析
前端·面试·性能优化
嘻嘻哈哈开森21 分钟前
Agent 系统技术分享
后端