Java 后端面试题(一)
Java 后端核心面试题
一、如何保证方法线程安全?
问题: 写了一个方法,如何保证该方法是线程安全的?
答案:
| 方式 | 说明 |
|---|---|
| 无共享变量 | 方法内部仅使用局部变量,不共享成员变量,天然线程安全 |
| 使用锁机制 | synchronized(隐式锁)、Lock(显式锁),保证同一时刻只有一个线程执行临界区代码 |
| 使用线程安全类 | 如 ConcurrentHashMap、CopyOnWriteArrayList、Atomic 原子类等 |
| 变量不可变 | 使用 final 修饰共享变量,禁止修改 |
| 线程私有 | 通过 ThreadLocal 让每个线程拥有独立变量副本,避免共享 |
二、synchronized 锁的级别及升级机制
问题: synchronized 是重量级锁还是轻量级锁?是一开始就是轻量级,还是一直不变?
答案:
synchronized 并非固定锁级别,JDK1.6 后做了锁优化,存在锁升级机制:
▼text复制代码偏向锁 → 轻量级锁 → 重量级锁(只能升级,不能降级)
| 锁级别 | 触发条件 | 实现方式 | 特点 |
|---|---|---|---|
| 偏向锁 | 无竞争时默认开启 | CAS 记录线程 ID | 几乎无开销 |
| 轻量级锁 | 出现轻微竞争(交替执行) | 自旋实现 | 无内核态切换 |
| 重量级锁 | 竞争激烈、自旋失败 | 依赖操作系统内核 | 阻塞线程,开销大 |
总结: 传统
synchronized是纯重量级锁,现在是自适应升级的混合锁。
三、@Transactional 异常未回滚问题分析
问题: 方法加 @Transactional,抛出异常但数据库未回滚,如何排查分析?
答案: 常见原因及排查方向:
| 序号 | 原因 | 说明 |
|---|---|---|
| 1 | 异常类型不对 | 默认仅捕获运行时异常(RuntimeException)和 Error,普通受检异常 Exception 不会回滚,需手动指定 rollbackFor = Exception.class |
| 2 | 异常被内部捕获 | 代码中 try-catch 吃掉异常,事务切面感知不到异常,不会触发回滚 |
| 3 | 事务失效(同类内部调用) | 同类方法内部调用加事务的方法,AOP 无法增强,事务失效 |
| 4 | 方法权限问题 | 事务注解加在 private / protected 方法上,Spring AOP 无法拦截 |
| 5 | 数据库引擎不支持事务 | 表引擎为 MyISAM,改为 InnoDB |
| 6 | 传播属性配置错误 | 传播行为设置为 NOT_SUPPORTED 等,强制非事务运行 |
| 7 | 多数据源 / 事务管理器配置异常 | 注解指定的事务管理器与数据源不匹配 |
四、同类调用、事务嵌套导致事务失效原因
问题: 调用同类方法为什么事务失效?事务嵌套为何也会失效?
4.1 同类内部调用失效
Spring 事务基于动态 AOP 代理实现,只有外部通过代理对象调用方法,才会被切面拦截、开启事务。同类中 this.方法() 是直接调用原对象,不走代理,事务注解无效。
解决方案:
- 自身注入自己
- 使用
AopContext获取代理对象 - 拆分方法到不同类
4.2 事务嵌套失效
主要由事务传播机制导致:
| 场景 | 行为 |
|---|---|
外层无事务、内层 REQUIRED | 内层独立事务,互不影响 |
外层有事务、内层 REQUIRED | 并入外层事务,内层异常会导致整体回滚 |
内层 REQUIRES_NEW | 新建独立事务,但部分场景因连接、代理问题出现异常 |
| 内层吞掉异常 | 外层无法感知,造成回滚异常 |
五、事务传播机制、四大特性、原子性理解
问题: 事务传播机制有几种?事务特性有哪些?原子性如何理解?
5.1 事务传播机制(共 7 种)
| 传播行为 | 说明 |
|---|---|
REQUIRED | 支持当前事务,不存在则新建(默认) |
SUPPORTS | 支持当前事务,不存在则以非事务运行 |
MANDATORY | 必须在事务中运行,不存在则抛异常 |
REQUIRES_NEW | 新建事务,挂起当前事务 |
NOT_SUPPORTED | 以非事务运行,挂起当前事务 |
NEVER | 以非事务运行,存在事务则抛异常 |
NESTED | 嵌套事务,外层回滚则内层也回滚,内层回滚不影响外层 |
5.2 事务四大特性(ACID)
| 特性 | 英文 | 说明 |
|---|---|---|
| 原子性 | Atomicity | 事务内所有操作要么全部成功,要么全部失败回滚 |
| 一致性 | Consistency | 事务执行前后,数据必须保持一致状态 |
| 隔离性 | Isolation | 并发事务之间互不干扰 |
| 持久性 | Durability | 事务提交后,数据永久保存 |
5.3 原子性理解
一个事务内的所有数据库操作要么全部成功提交,要么全部失败回滚,不可部分执行。比如下单扣库存、生成订单,两个操作必须同时生效或同时撤销。
六、Spring AOP 执行阶段 & 循环依赖解决方案
问题: Spring AOP 发生在 Bean 生命周期哪个阶段?Spring 循环依赖如何解决?
6.1 AOP 执行阶段
AOP 动态代理在 Bean 初始化完成后、放入单例池之前 生成代理对象。
▼text复制代码Bean 核心流程: 实例化 → 属性填充 → 初始化(@PostConstruct / init-method)→ AOP 代理 → 存入单例池
6.2 循环依赖解决
Spring 仅能解决单例 Bean 基于 setter 注入的循环依赖,依靠三级缓存:
| 缓存级别 | 存储内容 |
|---|---|
| 一级缓存 | 完整可用的单例 Bean |
| 二级缓存 | 已实例化、未完成属性填充的半成品 Bean |
| 三级缓存 | Bean 的工厂对象(提前暴露实例) |
原理: 实例化后提前暴露半成品,打破循环引用;构造器注入、多例 Bean 无法解决循环依赖。
七、项目中线程池使用、作用与选型原因
问题: 项目中如何使用线程池?为什么使用线程池?解决了什么问题?
7.1 使用方式
项目一般使用 ThreadPoolExecutor 手动创建线程池(禁止 Executors 默认工厂类,避免 OOM),根据业务设置核心线程、最大线程、队列、拒绝策略;用于异步处理、批量任务、文件导出、消息消费等场景。
7.2 使用原因 & 解决的问题
| 优势 | 说明 |
|---|---|
| 降低性能开销 | 避免频繁创建 / 销毁线程 |
| 统一管理线程 | 控制并发数量,防止无限创建线程导致 OOM |
| 提升吞吐量 | 复用线程、任务排队 |
| 提升稳定性 | 提供拒绝策略、监控、超时机制 |
八、大数据量导出 + 实时进度展示实现
问题: 大数据导出场景,如何实现展示导出进度(1%、5%…)?
答案: 整体采用异步导出 + 进度存储 + 前端轮询方案:
▼text复制代码1. 前端发起导出请求 → 后端开启异步线程执行导出任务 → 立即返回任务 ID 2. 每处理一批数据 → 将当前进度、任务状态存入 Redis / 内存 / 数据库(key = 任务 ID) 3. 前端根据任务 ID 轮询接口 → 查询 Redis 中的进度并展示 4. 导出完成 → 将文件地址写入缓存,前端拉取文件下载;异常则标记失败状态
优化: 数据分批查询、分批写入文件,避免一次性加载全量数据内存溢出。
九、死锁产生条件、排查、规避方案
问题: 什么情况会发生死锁?如何处理死锁?开发中如何避免?
9.1 死锁四大必要条件(同时满足才会死锁)
| 条件 | 说明 |
|---|---|
| 互斥条件 | 资源同一时刻只能被一个线程占用 |
| 请求与保持条件 | 持有资源的同时请求其他资源 |
| 不可剥夺条件 | 已获得的资源不能被强行释放 |
| 循环等待条件 | 多个线程形成循环等待资源的关系 |
9.2 线上处理死锁
| 场景 | 排查方式 |
|---|---|
| MySQL 死锁 | SHOW ENGINE INNODB STATUS 定位死锁 SQL |
| Java 线程死锁 | 使用 jstack、Arthas 排查阻塞线程与锁持有关系 |
| 临时恢复 | 重启服务、终止卡死请求 |
9.3 代码规避死锁(破坏四大条件)
- 统一锁顺序: 所有线程按固定顺序获取多把锁
- 加锁设置超时时间:
Lock.tryLock(time),超时自动释放 - 减少锁粒度: 尽量不嵌套锁
- 避免持有锁时再请求其他锁
十、MySQL 排他锁(FOR UPDATE)并发访问效果
问题: SQL 加排他锁 FOR UPDATE,第一个请求未执行完,第二个请求进来会继续执行还是阻塞?
答案:
| 锁类型 | 场景 | 第二个请求效果 |
|---|---|---|
| 行级排他锁 | 查询命中索引,锁对应数据行 | 访问同一行 → 阻塞等待;访问其他行 → 正常执行 |
| 表级排他锁 | 查询未命中索引 / 索引失效,行锁升级为表锁 | 所有后续请求都会阻塞 |
总结: 访问同一条数据必然阻塞;不同行、行锁生效则不阻塞。
十一、读锁、写锁(共享锁 & 排他锁)区别
问题: MySQL 读锁和写锁有什么区别?
读锁(共享锁 / LOCK IN SHARE MODE)
- 共享: 多个线程可同时加读锁,互不阻塞
- 互斥: 有加读锁时,所有写锁都会被阻塞
- 用途: 保证查询数据期间不被修改
写锁(排他锁 / FOR UPDATE)
- 独占: 只能有一个线程持有写锁
- 互斥: 持有写锁后,其他线程的读锁、写锁全部阻塞
- 用途: 更新、删除、强一致性查询场景
口诀: 读读共享、读写互斥、写写互斥
十二、MySQL 调优 & 索引数据结构
问题: MySQL 日常如何调优?索引使用什么数据结构?
12.1 MySQL 调优方向
| 优化方向 | 具体措施 |
|---|---|
| SQL 优化 | 避免 SELECT *、大事务、全表扫描、隐式类型转换;用 EXPLAIN 分析执行计划 |
| 索引优化 | 合理创建联合索引、遵循最左匹配,删除冗余 / 失效索引 |
| 架构优化 | 主从复制、读写分离、分库分表 |
| 配置调优 | 调整 Buffer Pool、连接数、超时时间等参数 |
| 存储引擎 | 业务表统一使用 InnoDB |
12.2 索引数据结构
InnoDB 主键索引、二级索引默认使用 B+ 树。
十三、B+ 树特点 & 和 B 树的区别
问题: B+ 树有哪些特点?对比 B 树有什么不同?
B+ 树特点
- 所有数据行 / 数据页都存在叶子节点,非叶子节点只存索引键,不存完整数据
- 叶子节点通过双向链表串联,范围查询、排序效率极高
- 树高度更低,磁盘 IO 次数更少,查询性能稳定
与 B 树核心区别
| 对比维度 | B 树 | B+ 树 |
|---|---|---|
| 数据存储 | 非叶子、叶子节点都存完整数据 | 仅叶子存数据 |
| 范围查询 | 需要中序遍历,效率低 | 叶子链表遍历,远快于 B 树 |
| IO 次数 | 节点存储数据多,索引少,树更高 | 节点存储索引更多,树更矮,磁盘 IO 更少 |
| 查询效率 | 单条查询速度不稳定 | 所有查询都从根走到叶子,性能均衡 |
十四、设计模式 + 实际业务场景
问题: 开发中用过哪些设计模式?举一个场景、模式、解决的问题。
示例 1:策略模式
- 场景: 订单支付(微信、支付宝、银行卡、余额多种支付方式)
- 使用模式: 策略模式
- 解决问题: 消除大量
if-else/switch判断,新增支付方式只需新增策略类,符合开闭原则,代码易维护扩展
示例 2:单例模式
- 场景: 全局配置类、工具类、连接池管理
- 使用模式: 饿汉 / 懒汉单例
- 解决问题: 保证全局只有一个实例,节省资源,统一全局配置
示例 3:工厂模式
- 场景: 根据类型创建不同消息推送(短信、APP 推送、站内信)
- 使用模式: 简单工厂 / 抽象工厂
- 解决问题: 对象创建与业务逻辑解耦,统一创建入口
十五、千万级宽表多条件分页查询优化
问题: 千万级订单主表、上百字段、多查询条件 + 关联表,分页十几秒,如何优化?
分维度整体优化
| 优化维度 | 具体措施 |
|---|---|
| 字段优化 | 宽表拆分为主表 + 扩展表,分页查询只查必要字段,避免查询上百字段 |
| 索引优化 | 根据高频查询条件建立联合覆盖索引,避免回表;优化关联表索引 |
| 分页优化 | 深分页使用主键偏移分页(WHERE id > ? LIMIT 10)替代传统 LIMIT offset, size |
| SQL 优化 | 简化多表关联,优先小表驱动大表;禁止不必要查询、排序 |
架构层面
- 冷热数据分离: 历史订单归档到历史表
- 引入 ES: 复杂多条件、模糊查询、分页全部走 ES,MySQL 仅做基础事务
- 读写分离: 查询走从库,减轻主库压力
- 缓存: 对高频固定条件的分页结果做缓存
十六、分页 COUNT 总数查询慢(全表扫描)优化
问题: 分页需要查 COUNT 统计总数,条件多变、全表检索很慢,索引已加,如何优化?
区分业务场景,非精准统计
| 方案 | 说明 |
|---|---|
| 允许近似值 | 使用 MySQL 信息统计表 information_schema.TABLES 获取粗略总数,速度极快 |
| 允许延迟更新 | 定时任务把总条数、条件统计结果预计算存入 Redis / 统计表,前端直接读取 |
精准 COUNT 优化
| 方案 | 说明 |
|---|---|
| 优先使用主键 / 非空唯一索引 | COUNT(主键) 优于 COUNT(*)、COUNT(字段) |
| 复杂多条件 | 将统计逻辑下沉到 ES / OLAP 数据库,专门做聚合统计 |
| 分页兜底 | 前端取消实时总数,使用「上拉加载更多」,不再查询 COUNT |
| 分区表 | 按时间分区,统计时只扫描对应分区,缩小扫描范围 |
十七、分库、分表的作用与解决的问题
问题: 为什么要分库?为什么要分表?分别解决什么问题?
分表(水平分表 / 垂直分表)
解决单表数据量过大问题(千万 / 亿级):单表数据越多,索引、查询、写入越慢,分表把数据拆分到多张结构相同的表,降低单表数据量,提升查询和写入性能。
- 垂直分表: 将不常用的大字段拆分到扩展表,主表保留高频字段
- 水平分表: 按规则(如 ID 取模、时间范围)将数据分散到多张结构相同的表
分库
解决单库连接数、IO、CPU 瓶颈问题:单库承载的并发连接数、磁盘 IO、CPU 计算都有上限,分库将数据分散到不同数据库实例,提升整体并发处理能力。
- 垂直分库: 按业务模块拆分(如订单库、用户库、商品库)
- 水平分库: 同一业务数据按规则分散到多个数据库实例
