# 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 ii++ 是线程安全的吗?

  • volatile 不能保证原子性

  • i++ = 读 → 加1 → 写,三步不是原子的,多线程下会丢失更新

  • 要用 AtomicInteger 或 synchronized

# 🟡 第二问(追问)

面试官:那 volatile 到底保证了什么?能解决下面这段代码的问题吗?

boolean flag = false; // 不加 volatile
// 线程 A
while(!flag) {}
// 线程 B
flag = true;
1
2
3
4
5
  • 能解决

  • 不加 volatile 时,线程 A 可能永远看不到 flag 的变化(JIT 优化或 CPU 缓存)

  • volatile 保证:

    1. 可见性:写操作立即刷新到主内存

    2. 禁止指令重排序(写屏障 + 读屏障)

  • 加了 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

最近更新: 9/19/2026, 1:27:08 PM
编程NOTE   |