美团高级Java开发工程师面试题及参考答案
一、Java基础部分
1. HashMap实现原理
题目:
- 请详细描述JDK8中HashMap的实现原理
- 为什么JDK8要将链表转为红黑树?阈值为什么是8?
- HashMap在多线程环境下会出现什么问题?如何解决?
参考答案:
-
JDK8 HashMap采用数组+链表+红黑树结构。当链表长度超过8时转为红黑树,小于6时转回链表。扩容因子默认0.75,扩容时大小翻倍,重新计算hash分布。
-
链表转红黑树是为了解决hash冲突严重时链表查询效率O(n)低的问题。阈值8是根据泊松分布计算得出,当hashCode离散良好时,链表长度达到8的概率极低(0.00000006)。
-
多线程问题:
- 死循环:JDK7扩容时链表倒置可能导致
- 数据丢失:并发put可能被覆盖
解决方案: - 使用Collections.synchronizedMap
- 使用ConcurrentHashMap
- 使用线程安全的HashTable(不推荐)
2. 并发编程
题目:
- 解释Java内存模型(JMM)中的happens-before原则
- ConcurrentHashMap在JDK7和JDK8中的实现区别
- 解释AQS工作原理及应用
参考答案:
-
happens-before原则包括:
- 程序顺序规则
- volatile变量规则
- 传递性规则等
保证指令重排序不会影响多线程程序的正确性。
-
ConcurrentHashMap区别:
- JDK7:分段锁(Segment),默认16段
- JDK8:CAS+synchronized,Node数组+链表+红黑树
-
AQS(AbstractQueuedSynchronizer):
核心是通过CLH队列和state变量实现锁机制,如:- ReentrantLock:可重入锁
- CountDownLatch:倒计时器
- Semaphore:信号量
二、JVM与性能调优
1. JVM内存模型
题目:
- 描述JVM内存结构及各区域作用
- 什么是内存泄漏?如何识别和避免?
- 解释G1垃圾回收器原理及优势
参考答案:
-
JVM内存结构:
- 堆:对象实例
- 方法区:类信息、常量等
- 虚拟机栈:方法调用
- 本地方法栈
- 程序计数器
-
内存泄漏指对象不再使用但无法被GC回收。识别方法:
- MAT分析堆dump
- 监控堆内存增长
避免方法: - 及时关闭资源
- 注意集合类引用
- 使用WeakReference
-
G1特点:
- 分Region收集
- 可预测停顿模型
- 标记-整理算法
优势:大堆内存、低延迟场景表现优异
三、分布式系统
1. 分布式锁实现
题目:
- 分布式锁有哪些实现方式?各有什么优缺点?
- Redis实现分布式锁要注意哪些问题?
参考答案:
-
实现方式:
- Redis:setnx+过期时间,性能好但可靠性依赖Redis
- Zookeeper:临时顺序节点,可靠性高但性能较差
- 数据库:唯一索引,简单但性能差
-
Redis实现注意事项:
- 原子性:setnx和expire要原子操作
- 超时时间:不宜过长或过短
- 释放锁:只能由加锁线程释放
- 锁续期:看门狗机制
- 集群问题:Redlock算法
四、系统设计
1. 高并发秒杀系统
题目:
- 设计一个秒杀系统,需要考虑哪些方面?
- 如何解决超卖问题?
参考答案:
-
设计要点:
- 流量削峰:队列缓冲
- 分层校验:先查缓存再查库
- 热点隔离:独立部署
- 限流降级:保护系统
-
超卖解决方案:
- 乐观锁:version字段
- Redis原子操作:decrement+lua
- 分布式锁:控制并发
- 预扣库存:内存计算
五、项目经验
题目:
- 请描述你处理过的最复杂的系统性能问题及解决方案
- 如何设计一个可扩展的微服务架构?
参考答案:
-
性能问题案例:
- 现象:接口响应慢,GC频繁
- 排查:分析GC日志,发现内存泄漏
- 解决:修复泄漏,优化JVM参数
- 结果:TPS提升3倍,GC时间减少80%
-
微服务设计要点:
- 服务拆分:业务边界清晰
- 通信机制:REST/gRPC
- 服务发现:Eureka/Nacos
- 配置中心:统一管理
- 熔断限流:Hystrix/Sentinel
- 链路追踪:Sleuth/Zipkin
六、编码题
题目:
实现一个线程安全的LRU缓存
参考答案:
public class ThreadSafeLRUCache<K, V> {
private final int capacity;
private final Map<K, V> cache;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public ThreadSafeLRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}