Java面试突击案例大全
我为你整理了一套高频面试题 + 实战案例,分为基础、进阶、框架、系统设计四个层次,每个案例都附有代码和解析。

Java基础高频面试题
HashMap底层原理(必考)
// 面试官:说一下HashMap的put流程
// 完整回答思路:
public class HashMapDemo {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("key1", 1);
// put流程:
// 1. 计算hash:hash(key) = (h = key.hashCode()) ^ (h >>> 16)
// 2. 如果table为null,调用resize()初始化,默认容量16
// 3. 计算索引:(n - 1) & hash
// 4. 如果该位置为null,直接插入
// 5. 不为null,判断是链表还是红黑树
// - 链表长度 >= 8 且数组长度 >= 64 → 转红黑树
// - 链表长度 >= 8 但数组长度 < 64 → 先扩容
// 6. 插入后size > threshold(容量*0.75),扩容为2倍
}
}
并发编程核心(必考)
// 问题:如何实现线程安全的单例?为什么推荐枚举方式?
public class SingletonDemo {
// 方式1:双重检查锁(DCL)- 需要volatile防止指令重排序
private static volatile SingletonDemo instance;
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo(); // 不是原子操作!
}
}
}
return instance;
}
// 方式2:静态内部类 - 线程安全且懒加载
private static class Holder {
private static final SingletonDemo INSTANCE = new SingletonDemo();
}
public static SingletonDemo getInstance2() {
return Holder.INSTANCE;
}
// 方式3:枚举(最推荐) - 天然防止反射和序列化破坏
// 详见下方
}
内存模型与垃圾回收(必考)
// 面试官:Java对象在内存中如何分配?
// 回答要点:
public class JVMDemo {
// 对象创建流程:
// 1. 类加载检查 → 2. 分配内存(指针碰撞 / 空闲列表)
// 3. 内存空间初始化(零值) → 4. 设置对象头
// 5. 执行<init>方法
// GC Roots有哪些?
// ① 虚拟机栈中引用的对象(局部变量)
// ② 方法区中静态属性引用的对象(static变量)
// ③ 方法区中常量引用的对象(final常量)
// ④ 本地方法栈中JNI引用的对象
// 判断对象存活:可达性分析(从GC Roots出发,不可达即回收)
// 不是引用计数法!(循环引用问题)
}
高效实战用例
场景1:千万级用户订单系统 - 并发处理
public class OrderService {
// 问题:如何保证订单号不重复?
// 方案1:数据库自增ID(分库分表时需使用雪花算法)
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 雪花算法生成唯一ID
public long generateOrderId() {
// Snowflake算法:1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号
// 每秒可生成409.6万个ID
return SnowflakeIdGenerator.getInstance().nextId();
}
// 防止超卖:分布式锁+乐观锁
public boolean createOrder(Long userId, Long productId, int count) {
// 方式1:Redis分布式锁(面试重点)
String lockKey = "lock:product:" + productId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
return false; // 获取锁失败
}
try {
// 检查库存(乐观锁:version字段)
// update t_product set stock = stock - #{count}
// where id = #{productId} and stock >= #{count}
// 扣减库存
// 创建订单
return true;
} finally {
// 释放锁(必须用Lua脚本保证原子性)
redisTemplate.delete(lockKey);
}
}
}
场景2:超时订单自动关闭 - 延迟队列
// 问题:如何高效处理超时未支付订单?
// 方案1:Redis过期监听(简单但不可靠)
// 方案2:消息队列延迟消息(推荐)
@Component
public class OrderTimeoutConsumer {
// RabbitMQ延迟队列实现
// 原理:消息先进入死信交换机,延迟时间后再投递到业务队列
@RabbitListener(queues = "order.timeout.queue")
public void handleTimeoutOrder(Order order) {
// 1. 检查订单状态
Order dbOrder = orderMapper.selectById(order.getId());
if (dbOrder.getStatus() == OrderStatus.UNPAID) {
// 2. 自动取消订单
orderMapper.updateStatus(order.getId(), OrderStatus.CANCELLED);
// 3. 恢复库存
stockService.restoreStock(order.getProductId(), order.getCount());
}
}
}
场景3:缓存穿透、击穿、雪崩的解决方案
@Service
public class CacheService {
// 1. 缓存穿透:查询不存在的数据
// 解决方案:布隆过滤器
public String getProductInfo(Long productId) {
// 先查布隆过滤器
if (!bloomFilter.mightContain("product:" + productId)) {
return null; // 肯定不存在,直接返回
}
// 2. 缓存空值(额外方案)
String value = redisTemplate.opsForValue().get("product:" + productId);
if (value == null) {
value = productMapper.selectById(productId);
if (value == null) {
// 缓存空值,设置较短过期时间(如5分钟)
redisTemplate.opsForValue().set("product:" + productId, "null", 300, TimeUnit.SECONDS);
return null;
}
}
return value;
}
// 2. 缓存击穿:热点key过期瞬间大量请求
// 解决方案:互斥锁
@Transactional
public String getHotProduct(Long productId) {
String key = "product:" + productId;
String value = redisTemplate.opsForValue().get(key);
if (value != null) return value;
// 加锁查询数据库(只让一个请求去查库)
String lockKey = "lock:" + key;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked) {
try {
value = productMapper.selectById(productId);
redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
return value;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 其他请求等等,然后重试
try { Thread.sleep(100); } catch (InterruptedException e) {}
return getHotProduct(productId); // 递归重试
}
}
// 3. 缓存雪崩
// 解决方案:
// - 设置不同的过期时间:baseTime + random
// - 缓存数据永不过期,后台定时刷新
public void setProductCache(Long productId, String value) {
int timeout = 3600 + new Random().nextInt(600); // 随机60-70分钟
redisTemplate.opsForValue().set("product:" + productId, value, timeout, TimeUnit.SECONDS);
}
}
分布式系统设计经典案例
案例:设计一个短链接系统
// 需求:长链接转为短链接,支持跳转
// 核心设计:
public class ShortUrlService {
// 1. 生成短码:使用发号器(Redis INCR)或雪花算法
public String createShortUrl(String longUrl) {
// 发号器生成唯一ID
long id = redisTemplate.opsForValue().increment("short:url:id");
// 将ID转为62进制(0-9a-zA-Z)
String shortCode = base62Encode(id); // 10进制→62进制
// 存储映射关系
redisTemplate.opsForValue().set("short:" + shortCode, longUrl);
return "https://t.cn/" + shortCode;
}
// 2. 跳转:redis找不到则查数据库
public String getLongUrl(String shortCode) {
String longUrl = redisTemplate.opsForValue().get("short:" + shortCode);
if (longUrl == null) {
// 查数据库,然后回填缓存
UrlMap urlMap = urlMapper.selectByShortCode(shortCode);
longUrl = urlMap.getLongUrl();
redisTemplate.opsForValue().set("short:" + shortCode, longUrl, 3600, TimeUnit.SECONDS);
}
return longUrl;
}
// 3. 10进制转62进制
private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private String base62Encode(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(CHARS.charAt((int)(num % 62)));
num /= 62;
}
return sb.reverse().toString();
}
}
线上故障排查实战
案例:CPU 100% 排查
# 1. 查看进程 top -c # 找到PID # 2. 查看该进程的线程 top -Hp PID # 3. 将线程ID转为16进制 printf "%x\n" 线程ID # 4. 查看线程堆栈 jstack PID | grep 线程ID十六进制 -A 50 # 5. 常见原因: # - 死循环 # - 频繁GC # - 线程阻塞(等待锁/IO)
代码示例 - 死循环排查:
// 一旦线上CPU飙升,很可能有类似代码
public class CPUTrouble {
// 死循环示例
public void deadLoop() {
while (true) {
// 业务逻辑
}
}
// 正确的做法:加上超时控制
public void correctLoop() {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 5000) { // 最多执行5秒
// 业务逻辑
}
}
}
最常问的第三问:Spring事务失效场景
@Service
public class TransactionDemo {
// 场景1:同类内部调用 - 事务失效
public void outerMethod() {
this.innerMethod(); // 这是直接调用,不走代理
}
@Transactional
public void innerMethod() {
// 数据库操作
}
// 解决:注入自身代理对象
@Autowired
private TransactionDemo self;
public void outerMethod2() {
self.innerMethod(); // 通过代理调用
}
// 场景2:final方法(CGLIB代理无法重写)或private方法
// 场景3:异常被捕获,未抛出
@Transactional
public void methodCatch() {
try {
int result = 1 / 0;
} catch (Exception e) {
// 异常被捕获,事务不会回滚
}
}
// 解决方法:抛异常
@Transactional
public void methodThrow() {
try {
int result = 1 / 0;
} catch (Exception e) {
throw new RuntimeException("异常", e); // 抛出RuntimeException
}
}
// 场景4:传播行为不当
// Propagation.NOT_SUPPORTED 不会启动事务
// 场景5:配置了错误的回滚条件
// @Transactional(rollbackFor = Exception.class) // 必须指定,否则只回滚RuntimeException
// 场景6:数据库引擎不支持事务(MyISAM)
}
线程池面试必问
public class ThreadPoolDemo {
// 问题1:为什么不用Executors.newFixedThreadPool?
// 1. 无界队列(LinkedBlockingQueue),OOM风险
// 2. 线程数量固定,无法灵活伸缩
// 正确方式:
@Bean
public ThreadPoolExecutor businessThreadPool() {
return new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60, // 空闲时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列,容量100
new NamedThreadFactory("business-"),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
// 问题2:线程池执行流程
// 提交任务 → 核心线程数不足?→ 创建核心线程执行
// → 核心线程满了?→ 放入队列
// → 队列满了?→ 创建非核心线程
// → 最大线程满了?→ 执行拒绝策略
// 问题3:常见拒绝策略
// ThreadPoolExecutor.AbortPolicy:直接抛异常(默认)
// DiscardPolicy:丢弃任务不抛异常
// DiscardOldestPolicy:丢弃最旧的任务(队列第一个)
// CallerRunsPolicy:调用者线程执行(不会丢任务,但会阻塞调用者)
}
MySQL索引优化实战
-- 最左匹配原则示例 CREATE INDEX idx_user_name_age ON t_user(name, age); -- 下面的SQL会用到索引 SELECT * FROM t_user WHERE name = '张三'; -- 用索引 SELECT * FROM t_user WHERE name = '张三' AND age = 18; -- 用索引 -- 下面的SQL不会用到索引 SELECT * FROM t_user WHERE age = 18; -- 跳过第一个字段,不走索引 -- 覆盖索引优化 -- 原SQL(回表查询慢) SELECT * FROM t_order WHERE user_id = 100; -- 优化为覆盖索引 CREATE INDEX idx_order_user_status ON t_order(user_id, status); SELECT user_id, status FROM t_order WHERE user_id = 100; -- 只查索引列,避免回表 -- 分页优化 -- 深度分页(慢) SELECT * FROM t_order ORDER BY id LIMIT 100000, 20; -- 优化:延迟关联 SELECT * FROM t_order WHERE id > (SELECT id FROM t_order ORDER BY id LIMIT 100000, 1) ORDER BY id LIMIT 20;
消息队列可靠性保障
@Component
public class MQProducter {
// 问题:如何保证消息不丢失?
// 发送端:
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrderMessage(Order order) {
CorrelationData correlationData = new CorrelationData(order.getId());
// 开启confirm模式:发送成功后回调确认
rabbitTemplate.convertAndSend(
"order.exchange",
"order.route",
order,
correlationData
);
}
@PostConstruct
public void init() {
// Confirm确认回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
// 消息发送失败,重试或入库
System.err.println("消息发送失败: " + cause);
}
});
}
// 消费端:手动ACK
@RabbitListener(queues = "order.queue")
public void consume(Order order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
try {
// 处理业务
orderService.handleOrder(order);
// 手动确认
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
// 处理失败,要求重试或进入死信队列
channel.basicNack(deliveryTag, false, true); // 重新入队
}
}
}
系统设计真题
设计一个秒杀系统
// 架构层次:
// 1. 前端层:按钮置灰 + 答题 + 限流(滑动窗口)
// 2. 网关层:负载均衡 + 限流
// 3. 应用层:分布式锁 + 库存预减
// 4. 数据层:Redis库存预热 + 异步下单
@Service
public class SeckillService {
// Redis预减库存
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
// 商品数量预先加载到Redis
public void preLoadStock(Long productId, int count) {
redisTemplate.opsForValue().set("seckill:stock:" + productId, String.valueOf(count));
}
// 秒杀入口
public Result seckill(Long userId, Long productId) {
// 1. 限流:redis计数器
// 2. 预减库存(原子操作)
Long stock = redisTemplate.opsForValue()
.decrement("seckill:stock:" + productId);
if (stock < 0) {
// 库存不足,恢复数量
redisTemplate.opsForValue().increment("seckill:stock:" + productId);
return Result.error("已售罄");
}
// 3. 发送MQ异步处理订单
SeckillMessage message = new SeckillMessage(userId, productId);
rocketMQTemplate.convertAndSend("seckill-topic", message);
return Result.success("排队中");
}
// MQ消费者:真正落库处理
@RocketMQMessageListener(topic = "seckill-topic", consumerGroup = "seckill-group")
public class SeckillConsumer implements RocketMQListener<SeckillMessage> {
@Override
public void onMessage(SeckillMessage message) {
// 1. 创建订单(唯一索引防重复)
// 2. 扣减数据库库存
// UPDATE stock SET count = count - 1 WHERE product_id = ? AND count > 0
// 3. 发送成功通知
}
}
}
消息队列必答 - Kafka/RabbitMQ对比
// 面试官:说一下Kafka和RabbitMQ的区别和选型 面试要点: 1. 吞吐量:Kafka(百万级/秒) > RabbitMQ(万级/秒) 2. 消息顺序:Kafka分区内有序;RabbitMQ需要特殊配置 3. 消息确认:Kafka offset机制;RabbitMQ手动ACK 4. 延迟:Kafka毫秒级;RabbitMQ微秒级 5. 功能特性:RabbitMQ功能丰富(延迟、死信、优先级);Kafka简单 6. 扩展性:Kafka水平扩展好(分区机制) 选型建议: - 需要高吞吐、日志收集 → Kafka - 业务复杂、需精确路由 → RabbitMQ - 延迟敏感业务 → RabbitMQ
十一、JVM调优实战
// 线上OOM排查案例
public class OOMAnalyze {
// 出现OOM后:
// 命令添加参数:
// -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=/tmp/heapdump.hprof
// 分析堆转储文件
// jmap -dump:format=b,file=/tmp/heap.hprof PID
// 使用MAT分析:
// - 找大对象
// - 找重复字符串
// - 找未释放的集合
// 常见OOM场景:
// 1. 堆内存不足(对象过多)
// 2. 元空间不足(动态生成类)
// 3. 直接内存(NIO使用不当)
}
十二、Redis高级特性必答
// 面试官:Redis中热Key和大Key如何解决? // 热Key问题: // 1. 本地缓存:JVM层缓存热点数据 // 2. 多副本:多个Redis副本分担读压力 // 3. 读写分离:master写,slave读 // 大Key问题: // 1. 拆分:将大对象拆分成多个key // 2. 压缩:序列化时使用压缩算法(protobuf、snappy) // 3. 分批:删除大集合时用scan而非del // Sorted Set底层实现(树和跳跃表的选择原因) // 综合对比:跳跃表实现简单、查找效率低,但范围查找效率高 // 因此Redis选择跳表 + 哈希表 // 一致性哈希在Redis中的应用: // 一致性哈希解决分布式缓存中节点的动态增删问题 // 虚拟节点解决数据倾斜问题
十三、Spring Boot + MyBatis-Plus经典案例
// 实现多租户数据隔离
@Configuration
public class TenantConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(
new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从上下文获取租户ID
return new LongValue(TenantContext.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id"; // 自动拼接tenant_id条件
}
}
));
return interceptor;
}
}
// 分页插件
@Configuration
public class PaginationConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
十四、面试中要掌握的框架原理
Spring Boot自动配置原理
面试回答思路:
1. @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
2. @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class)
3. 加载 META-INF/spring.factories 中所有自动配置类
4. 结合 @ConditionalOnXxx 条件判断哪些配置生效
5. 自动配置类中通过 @Bean 生成对应的 Bean
Spring循环依赖解决方案
// 面试句重点:
// 三级缓存:
// 一级缓存: singletonObjects 存储完整Bean
// 二级缓存: earlySingletonObjects 存储半成品Bean(未填充属性)
// 三级缓存: singletonFactories 存储ObjectFactory(lambda表达式)
@Lazy // 最简单的解决方案:懒加载打破循环
public class A {
@Autowired
private B b;
}
// 循环依赖无法解决的情况(完全类比构造函数循环依赖)
MyBatis一级缓存和二级缓存
一级缓存:SqlSession级别,默认开启,同一个SqlSession查询会命中缓存
二级缓存:namespace级别,需要配置,且实体类需要序列化
注意:多表关联查询时缓存可能导致脏数据
十五、面试中的高并发实战案例
案例:热搜榜设计
// 问题:如何实现热搜排行榜?
// 方案:利用Redis的ZSet结构
public class HotSearchService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String HOT_KEY = "hot:search";
// 1. 搜索时增加热度
public void addHot(String keyword) {
redisTemplate.opsForZSet().incrementScore(HOT_KEY, keyword, 1.0);
}
// 2. 获取Top10热搜
public List<String> getTop10() {
Set<String> top = redisTemplate.opsForZSet()
.reverseRange(HOT_KEY, 0, 9); // 分数从高到低
return new ArrayList<>(top);
}
// 3. 热度随时间衰减
// 可以使用定时任务:每小时把所有词的分数乘以0.9
// 4. 防止某个词在某个时间段被重复刷
// 可加上单日搜索量限制
}
面试拿到Offer的关键
| 要素 | 重要性 | 怎么准备 |
|---|---|---|
| 基础功底 | 多线程、集合、JVM原理必须烂熟于心 | |
| 框架原理 | 能讲出Spring/MyBatis核心原理 | |
| 系统设计 | 面试前准备5-8个业务场景 | |
| 实战经验 | 独立完成至少2-3个项目,深入理解每一行代码 | |
| 思路清晰 | 多练习,用金字塔原理回答,先说结论再说原因 |
面试禁忌
- 不要背答案:结合自己的项目去阐述,哪怕是实习项目
- 不要说"我不会":尽量说"这块我了解的是…"
- 不要一直强调不会的:引导面试官问你擅长的领域
- 不要紧张:提前做几轮模拟面试
最后的考前面试清单
- [ ] 把HashMap、ConcurrentHashMap源码看一遍
- [ ] 能默写线程池的7个参数并解释执行流程
- [ ] Spring IOC/AOP源码核心流程过一遍
- [ ] MySQL索引结构(B+树)能画出来并讲清楚
- [ ] Redis数据类型及典型应用场景
- [ ] 准备2-3个系统设计案例(秒杀、分布式锁、限流)