# 03.面试八股
# 🟢 第一问
面试官:HashMap 是线程安全的吗?多线程环境下会有什么问题?
不是线程安全的
- jdk 1.7 会导致死循环(头插法)
- jdk 1.8 仍然会导致put操作数据覆盖的问题
# 🟡 第二问(追问)
面试官:你说 JDK 1.8 解决了死循环,那 1.8 的扩容还是线程安全的吗?为什么?
不是线程安全的
数据覆盖:计算出哈希值,同时判断出某结点为空,后写入覆盖先写入
结构紊乱:同时扩容时,可能会发生红黑树结构紊乱,指针错误
size计数不准:++size不是原子操作
# 🔴 第三问(再追问)
面试官:那 ConcurrentHashMap 是怎么解决这些问题的?JDK 1.7 和 1.8 的实现有什么区别?
jdk1.7 使用分段锁,先根据key的hash值定位到具体的Segment(一共有16个),然后对这个段进行加锁插入。
jdk1.8 采用了CAS + sync悲观锁,当key对应的hash值定位到的索引没有元素时,会直接插入(配合CAS机制进行检测,防止同时插入的情况),当索引对应的位置有元素时,会将此索引位置的头结点加上sync锁,防止其他元素插入,当其他线程插入其他位置正好触发扩容时,线程会进行二次检查,检查出扩容后,此节点插入失败,先协助扩容再尝试插入
# 🔴 第四问(深度追问)
面试官:ConcurrentHashMap 的 size() 是怎么实现的?为什么不是直接返回一个全局 size 变量?
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 核心思想 | 分段锁,每个段独立计数 | 类似 LongAdder,分散计数 |
| 计数变量 | Segment.count | baseCount + CounterCell[] |
| 实现方式 | 无锁多次尝试,不一致则加锁 | CAS 更新基础值,失败则更新分散的 Cell |
| 结果准确性 | 近似值,高并发时可能加锁 | 近似值,性能极高 |
# 🔥 话题二:volatile 与 happens-before
# 🟢 第一问
面试官:volatile 能保证原子性吗?volatile int i,i++ 是线程安全的吗?
volatile 不能保证原子性
i++= 读 → 加1 → 写,三步不是原子的,多线程下会丢失更新要用
AtomicInteger或 synchronized
# 🟡 第二问(追问)
面试官:那 volatile 到底保证了什么?能解决下面这段代码的问题吗?
boolean flag = false; // 不加 volatile
// 线程 A
while(!flag) {}
// 线程 B
flag = true;
2
3
4
5
能解决
不加 volatile 时,线程 A 可能永远看不到 flag 的变化(JIT 优化或 CPU 缓存)
volatile 保证:
可见性:写操作立即刷新到主内存
禁止指令重排序(写屏障 + 读屏障)
加了 volatile 后,线程 A 能正常退出
# 🔴 第三问(深度追问)
面试官:volatile 的“禁止指令重排序”是怎么实现的?JMM 层面的内存屏障是什么?
| 屏障类型 | 插入位置 | 核心作用 |
|---|---|---|
| StoreStore | volatile 写操作之前 | 禁止上面的普通写与下面的 volatile 写重排 |
| StoreLoad | volatile 写操作之后 | 禁止上面的 volatile 写与下面的读写重排(开销最大) |
| LoadLoad | volatile 读操作之后 | 禁止上面的 volatile 读与下面的普通读重排 |
| LoadStore | volatile 读操作之后 | 禁止上面的 volatile 读与下面的普通写重排 |
# 🟢 第一问
面试官:synchronized 用过吗?和 ReentrantLock 的区别是什么?
自动释放、可中断、超时获取、公平锁、锁升级
# 第二问(追问)
面试官:你说的“锁升级”具体是怎么变化的?无锁 → 偏向锁 → 轻量级锁 → 重量级锁,分别在什么条件下发生?
当对象创建时且没有线程时,进入无锁状态
当有一个线程时进入偏向锁状态
当有第二个线程来争抢时,CAS自旋自动尝试获取锁,进入轻量级锁状态
当系统并发过高,或者CAS自旋失败次数过多,进入重量级锁状态
# 🔴 第三问(深度追问)
面试官:轻量级锁的 CAS 自旋会一直占用 CPU,JVM 怎么优化这个问题的?
自适应自旋:上一次自旋成功过 → 多自旋几次;失败过 → 少自旋或不自旋
当CAS自旋到达一定次数后会进入重量级锁状态
# 🟢 第一问
面试官:ThreadPoolExecutor 的 7 个核心参数是什么?
核心线程数、最大线程数、空闲线程存活时间、时间单位、阻塞队列、拒绝策略、线程工厂
# 🟡 第二问(追问)
面试官:如果我把 corePoolSize 设成 10,maxPoolSize 设成 20,workQueue 是无界队列(LinkedBlockingQueue),能跑到 20 个线程吗?
不能
首先如果线程数小于核心线程,先创建核心线程,核心线程满,任务入队,任务队列满了,才会创建非核心线程,但是你说的是无边队列,所以任务队列满不了,就不会创建非核心线程
# 🔴 第三问(深度追问)
面试官:线上如何监控线程池状态?核心线程数设多大合适(CPU 密集 vs IO 密集)?
监控:
- 线程池提供的
getPoolSize()、getActiveCount()、getQueue().size()
核心数估算:
CPU 密集:
core ≈ CPU 核心数 + 1(线程太少执行不过来,太多会上下文切换频繁)IO 密集:
core ≈ CPU 核心数 × 2(或更大,因为线程经常阻塞等待 IO)
(因为线程经常需要等待,多开一些线程可以保持 CPU 的忙碌,掩盖 IO 等待的时间。)
ThreadLocal的原理和内存泄漏
原理:每个线程都会在本地线程存储一个类似于HashMap的副本来存储键值对,其中key实例本身是弱引用,值是强引用
内存泄漏:由于key是弱引用,只能保存到下一次垃圾回收时,但是value是强引用,他一般不会被回收,这就导致了清空了key,但是value仍然存在,如果是一个短线程还好说,但是如果他是一个生命周期长的线程,就会持续保存这个value,不会被清理,久而久之导致内存溢出
解决方案:在每次ThreadLocal使用之后,回收remove