20260410
04月10日面试
1.手写一个线程安全的计数器(两种方法)
1)使用synchronized(最基础、必掌握)
▼java复制代码public class Counter { private int count = 0; // 加一 public synchronized void increment() { count++; } // 获取值 public synchronized int getCount() { return count; } }
面试回答要点:
- 使用
synchronized保证原子性、可见性、有序性 count++是三步操作(读→改→写),非原子,必须加锁- 简单可靠,但高并发下性能一般
2)使用 AtomicInteger(推荐,性能更好)
▼java复制代码import java.util.concurrent.atomic.AtomicInteger; public class Counter { private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }
面试回答要点:
- 基于 CAS 无锁机制,性能比 synchronized 高
- 利用 CPU 原语保证原子操作,避免线程阻塞
- 适合高并发计数场景
2.手写线程安全的单例模式
1)双重校验锁(DCL)+ volatile(最常考)
▼java复制代码public class Singleton { // 禁止指令重排,保证多线程可见性 private static volatile Singleton instance; // 私有构造,禁止外部 new private Singleton() {} public static Singleton getInstance() { // 第一次判断,避免每次加锁 if (instance == null) { synchronized (Singleton.class) { // 第二次判断,防止多线程同时进入外层 if if (instance == null) { instance = new Singleton(); } } } return instance; } }
面试必说要点:
- 构造器私有,防止外部实例化
volatile禁止指令重排,避免 DCL 失效- 两次判空 + 锁类对象,保证线程安全
- 懒加载,节约内存
2)静态内部类
▼java复制代码public class Singleton { private Singleton() {} // 静态内部类,只有被调用时才加载 private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }
优点:
- 由 JVM 类加载机制 保证线程安全
- 懒加载、性能高、代码简洁
- 无锁,面试加分项
3)饿汉式(最简单,但不是懒加载)
▼java复制代码public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
特点:
- 天生线程安全,但类加载就初始化,可能浪费内存。
3.MySQL的ACID特性,InnoDB 如何保证
一、ACID 是什么
- A(Atomicity)原子性:事务要么全部成功,要么全部失败回滚,不允许部分执行。
- C(Consistency)一致性:事务执行前后,数据库完整性约束不变。
- I(Isolation)隔离性:多个事务并发执行时,互相不可见、不干扰。
- D(Durability)持久性:事务一旦提交,结果永久保存,宕机不丢失。
二、InnoDB 如何保证 ACID
- 原子性(A) 保证:undo log(回滚日志) 执行更新前,先把旧数据记入 undo log 事务失败时,通过 undo log 回滚到之前状态 配合 redo log 保证崩溃后仍能正确回滚
- 一致性(C) 保证:原子性 + 隔离性 + 持久性 + 约束检查 事务满足 AID 自然保证一致性 外加主键、外键、唯一约束、触发器等
- 隔离性(I) 保证:锁 + MVCC(多版本并发控制) 读写锁、间隙锁、临键锁解决脏写、幻读等问题 MVCC 实现读写不阻塞,通过 undo log 生成历史版本 配合四大隔离级别: Read Uncommitted Read Committed(RC) Repeatable Read(RR,InnoDB 默认) Serializable
- 持久性(D) 保证:redo log(重做日志) + 刷盘机制 事务提交前,先写 redo log(WAL 预写日志) 即使 buffer pool 数据未刷盘,宕机重启后也能通过 redo log 恢复 配合 binlog 保证主从一致
4.JVM内存模型,OOM的区域以及原因
一、JVM 内存区域划分(线程私有 + 线程共享)
-
线程私有区域(每个线程独立) 程序计数器 记录当前线程执行字节码行号。 虚拟机栈 存放局部变量、方法出口、栈帧。 本地方法栈 作用同虚拟机栈,为 native 方法服务。
-
线程共享区域(所有线程共用) 堆(Heap) 存放对象实例,GC 主要区域,分为新生代 + 老年代。 方法区(元空间 MetaSpace) 存储类信息、常量、静态变量、即时编译代码。 JDK8 后叫元空间,使用本地内存。 二、OOM 常见区域及原因(高频)
-
堆 OOM(Java heap space) 原因: 对象创建过多,且无法被 GC 回收(内存泄漏) 集合对象全局引用,一直持有不释放 堆内存设置过小(-Xmx 太小) 死循环不断创建对象 典型场景: 从数据库一次查询百万条数据加载进内存 静态 Map 无限缓存对象不清理
-
虚拟机栈 OOM / StackOverflowError StackOverflowError: 方法递归调用无出口,栈深度超限 OOM: 线程不断创建,栈内存耗尽
-
方法区 / 元空间 OOM(Metaspace) 原因: 大量动态生成类(CGLib、MyBatis、动态代理) 大量 JSP、反射类加载 常量池数据过多 元空间配置太小
-
直接内存 OOM 原因: NIO 框架频繁申请堆外内存 堆外内存未正确释放 总物理内存不足
5.ConcurrentHashMap
一、核心特点
- 线程安全的高效哈希表,替代 HashTable、同步 HashMap
- 读操作无锁,写操作分段锁 / CAS + synchronized,并发性能远高于 HashTable
- 键值不能为 null
二、JDK 1.7 实现
- 分段锁(Segment),继承自 ReentrantLock
- 默认 16 个 Segment,并发度 16
- 结构:Segment → 数组 + 链表
- 锁粒度:锁住一个 Segment,多段可同时操作
三、JDK 1.8 实现(重点必问)
- 取消 Segment,采用 CAS + synchronized 控制并发
- 结构:数组 + 链表 + 红黑树
- 锁粒度:只锁住链表 / 树的头节点,并发更高
- put 流程 根据 key 计算 hash 节点为空:CAS 插入 节点不为空:synchronized 锁住头节点 写入 链表长度 ≥ 8 且数组长度 ≥ 64 → 转为红黑树 扩容:达到负载因子 0.75 触发扩容
- get 流程 无锁,直接根据 hash 查找 利用 volatile 修饰数组 保证可见性
四、关键机制
-
hash 计算
高纬位参与运算,减少冲突
-
扩容机制
扩容为原来 2 倍
多线程协助迁移,提高效率
-
线程安全保证
读:volatile 保证可见性
写:CAS 无锁 + 轻量级锁 synchronized
五、对比总结
| 对比 | HashMap | HashTable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(全表锁) | 是(细粒度锁) |
| 性能 | 高 | 低 | 高并发优秀 |
| 键值 null | 允许 | 不允许 | 不允许 |
| JDK8 结构 | 数组 + 链表 + 红黑树 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
