1. JMM基础概念与核心问题
Java内存模型(JMM)是理解多线程编程的基石,它定义了线程如何与内存交互的规范。在实际面试中,面试官往往会从以下几个维度考察候选人对JMM的理解深度:
1.1 为什么需要内存模型
现代计算机体系结构中,CPU的运算速度远高于内存访问速度。为了弥补这个差距,CPU引入了多级缓存架构。以典型的x86架构为例:
- 每个CPU核心有独立的L1/L2缓存(纳秒级访问)
- 共享的L3缓存(约10ns)
- 主内存访问需要100ns以上
这种架构带来了可见性问题:线程A修改了变量值可能不会立即被线程B看到。JMM通过happens-before规则建立跨线程的内存可见性保证,就像交通信号灯协调不同方向的车辆通行。
1.2 JMM的三大特性
-
原子性:基本类型(除long/double)的读写是原子的,但i++这样的复合操作需要同步。例如:
java复制// 非原子操作示例 class Counter { private int value; void increment() { value++; } // 实际包含read-modify-write三步 } -
可见性:volatile变量的写操作会立即刷新到主内存,读操作会从主内存重新加载。对比普通变量可能只在工作内存中操作。
-
有序性:编译器/处理器会进行指令重排序。JMM通过内存屏障限制重排序范围,比如:
- StoreStore屏障:禁止上面的普通写与下面的volatile写重排序
- LoadLoad屏障:禁止上面的volatile读与下面的普通读重排序
1.3 常见面试问题模式
面试题目通常围绕以下模式设计:
- 给出一段有并发问题的代码,要求指出问题并修复
- 对比volatile、synchronized、final等关键字的语义差异
- 分析特定场景下的happens-before关系
- 解释经典案例(如双重检查锁、线程池关闭等)的正确实现方式
提示:回答JMM问题时,建议先明确问题涉及的特性(原子性/可见性/有序性),再结合具体技术点展开分析。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 原子性问题实战解析
2.1 竞态条件典型案例
下面这个银行转账案例展示了典型的原子性问题:
java复制class BankAccount {
private int balance;
void transfer(BankAccount target, int amount) {
if (this.balance >= amount) {
this.balance -= amount; // 步骤1
target.balance += amount; // 步骤2
}
}
}
问题在于:两个步骤不是原子操作,可能被其他线程中断,导致余额总和变化。我曾在一个支付系统中遇到过因此导致的资金不平衡问题,最终通过以下方案解决:
2.2 解决方案对比
| 方案 | 实现方式 | 适用场景 | 性能影响 |
|---|---|---|---|
| synchronized | 方法或代码块加锁 | 简单业务逻辑 | 高(上下文切换开销) |
| ReentrantLock | 显式锁控制 | 需要尝试锁、超时等高级特性 | 中等 |
| CAS操作 | AtomicInteger等原子类 | 计数器等简单场景 | 低(CPU自旋开销) |
| ThreadLocal | 线程隔离变量 | 避免共享的场景 | 几乎无影响 |
2.3 AtomicInteger实现原理
以AtomicInteger为例,其核心实现依赖于Unsafe类的CAS操作:
java复制public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// HotSpot源码中的CAS实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
实际项目中要注意:
- 高并发下CAS可能因自旋消耗CPU(ABA问题可通过AtomicStampedReference解决)
- 复合操作仍需外层同步(比如先检查后操作的场景)
3. 可见性问题深度剖析
3.1 内存可见性示例
下面这段代码可能永远无法停止:
java复制public class VisibilityDemo {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!stop); // 可能读取到线程本地缓存中的旧值
System.out.println("Thread stopped");
}).start();
Thread.sleep(1000);
stop = true; // 主线程修改
}
}
3.2 解决方案对比
-
volatile关键字:
- 保证变量的读写直接作用于主内存
- 插入内存屏障防止指令重排序
- 适合单个变量的状态标志位
-
synchronized同步:
- 进入monitor时会清空工作内存
- 退出时会将修改刷到主内存
- 适合需要原子性+可见性的复合操作
-
final字段:
- 正确构造的对象,final字段初始化后对其他线程可见
- 需要防止this引用逸出(构造函数中发布this引用)
3.3 happens-before规则应用
JMM定义的happens-before关系包括:
- 程序顺序规则:同一线程中的操作按程序顺序
- 锁规则:unlock操作先于后续的lock操作
- volatile规则:写操作先于后续的读操作
- 线程启动规则:Thread.start()先于线程内任何操作
- 传递性规则:A先于B,B先于C,则A先于C
案例分析:
java复制class HBExample {
int x = 0;
volatile boolean v = false;
void writer() {
x = 42; // 1
v = true; // 2
}
void reader() {
if (v) { // 3
System.out.println(x); // 4
}
}
}
由于volatile的happens-before规则,如果线程A调用writer后线程B调用reader,B一定能看到x=42。
4. 有序性问题与指令重排序
4.1 重排序类型
- 编译器优化重排序:在不改变单线程语义前提下调整指令顺序
- 处理器指令级并行:现代CPU的流水线、多发射等特性
- 内存系统重排序:由于缓存的存在使得写操作看起来延迟
4.2 双重检查锁定问题
经典的错误实现:
java复制class Singleton {
private static Singleton instance;
static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) // 第二次检查
instance = new Singleton(); // 问题出在这里!
}
}
return instance;
}
}
问题在于new Singleton()可能被重排序为:
- 分配内存空间
- 将引用指向内存(此时instance非null)
- 初始化对象
解决方案:
- 使用volatile修饰instance
- 改用静态内部类方式(类加载机制保证线程安全)
- 枚举单例(最安全的方式)
4.3 内存屏障实战
JVM插入的内存屏障类型:
| 屏障类型 | 示例场景 | 作用 |
|---|---|---|
| LoadLoad | volatile读后接普通读 | 禁止下面的普通读与上面的volatile读重排序 |
| StoreStore | volatile写前有普通写 | 禁止上面的普通写与下面的volatile写重排序 |
| LoadStore | volatile读后接普通写 | 禁止下面的普通写与上面的volatile读重排序 |
| StoreLoad | volatile写后可能有读 | 禁止上面的写与下面的读重排序(全能型屏障) |
在x86架构下,由于较强的内存模型,只有StoreLoad屏障需要实际插入lock指令(如volatile写后的屏障)。
5. 综合案例分析
5.1 生产者消费者模式
正确实现需要考虑:
- 队列操作的线程安全
- 空/满条件判断的准确性
- 通知机制的正确使用
使用BlockingQueue的简单实现:
java复制class ProducerConsumer {
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
void produce() throws InterruptedException {
while (true) {
int item = produceItem();
queue.put(item); // 自动阻塞
}
}
void consume() throws InterruptedException {
while (true) {
Integer item = queue.take(); // 自动阻塞
processItem(item);
}
}
}
5.2 并发计数器优化
高并发场景下的计数器优化方案:
-
LongAdder:分段计数减少竞争,适合高写场景
java复制LongAdder counter = new LongAdder(); counter.increment(); long sum = counter.sum(); // 注意:非原子快照 -
ConcurrentHashMap:利用分段思想
java复制ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>(); map.compute("key", (k, v) -> v == null ? 1 : v + 1); -
自定义方案:结合ThreadLocal和定期汇总
java复制class ThreadLocalCounter { private final ThreadLocal<Long> localCount = ThreadLocal.withInitial(() -> 0L); private final AtomicLong globalCount = new AtomicLong(); void increment() { localCount.set(localCount.get() + 1); if (localCount.get() % 100 == 0) { // 定期提交 globalCount.addAndGet(localCount.get()); localCount.set(0L); } } }
5.3 线程池关闭的正确姿势
常见问题场景:
- 任务未完成时直接shutdownNow()导致数据不一致
- 忽略未捕获异常导致线程悄悄死亡
- 未正确处理拒绝策略
推荐做法:
java复制ExecutorService pool = Executors.newFixedThreadPool(4);
try {
// 提交任务...
pool.shutdown(); // 温和关闭
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 强制关闭
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未正常关闭");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
6. 高频面试题精讲
6.1 volatile和synchronized区别
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 仅保证单次读/写原子性 | 保证代码块原子性 |
| 可见性 | 直接保证 | 通过锁机制间接保证 |
| 有序性 | 限制重排序 | 限制重排序 |
| 阻塞 | 不会阻塞 | 可能阻塞 |
| 适用场景 | 状态标志位 | 复合操作 |
6.2 ThreadLocal内存泄漏问题
典型的内存泄漏链:
Thread -> ThreadLocalMap -> Entry(key为弱引用, value为强引用) -> Value对象
正确使用方式:
- 使用后及时调用remove()
- 尽量使用static final修饰ThreadLocal
- 考虑使用Netty的FastThreadLocal等优化实现
6.3 CAS的ABA问题
问题描述:
- 线程1读取值为A
- 线程2修改为B后又改回A
- 线程1的CAS操作仍然成功
解决方案:
-
使用AtomicStampedReference带版本号
java复制AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); int[] stampHolder = new int[1]; String value = ref.get(stampHolder); // 同时获取值和版本戳 ref.compareAndSet("A", "B", stampHolder[0], stampHolder[0]+1); -
对于引用类型,可以利用地址不变的特性
7. 性能优化实战技巧
7.1 减少锁竞争
-
锁细化:将大锁拆分为多个小锁
java复制// 不推荐 synchronized(this) { /* 大量代码 */ } // 推荐 private final Object readLock = new Object(); private final Object writeLock = new Object(); -
锁分离:读写锁分离
java复制ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); // 多个读线程可同时进入 lock.writeLock().lock(); // 写线程独占 -
无锁数据结构:如ConcurrentLinkedQueue
7.2 伪共享问题
CPU缓存行(通常64字节)导致的性能问题:
java复制// 两个变量可能位于同一缓存行
class FalseSharing {
volatile long x; // 占用8字节
volatile long y; // 与x相邻
}
解决方案:
-
填充无用字段使变量独占缓存行
java复制class PaddedAtomicLong extends AtomicLong { private long p1, p2, p3, p4, p5, p6 = 7L; // 填充56字节 } -
使用@Contended注解(JDK8+)
java复制@sun.misc.Contended class ContendedDemo { volatile long value; }
7.3 并发容器选型指南
| 场景 | 推荐容器 | 特点 |
|---|---|---|
| 高频读少写 | CopyOnWriteArrayList | 写时复制开销大 |
| 队列 | ConcurrentLinkedQueue(无界) / LinkedBlockingQueue(有界) | 前者CAS实现,后者锁实现 |
| 映射 | ConcurrentHashMap | 分段锁/红黑树优化 |
| 排序 | ConcurrentSkipListMap | 跳表实现有序 |
| 计数 | LongAdder | 高并发下优于AtomicLong |
8. 常见陷阱与调试技巧
8.1 死锁诊断
产生死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
诊断方法:
-
jstack获取线程转储
bash复制
jstack <pid> > thread_dump.txt -
查找"deadlock"关键词
-
分析锁持有和等待关系
预防措施:
- 按固定顺序获取锁
- 使用tryLock()带超时
- 静态代码分析工具检测
8.2 线程池参数设置
错误配置示例:
java复制// 问题1:无界队列可能导致OOM
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
// 问题2:核心线程数过大导致上下文切换开销
new ThreadPoolExecutor(100, 100, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
推荐策略:
- CPU密集型:核心数 = CPU核数 + 1
- IO密集型:核心数 = CPU核数 * (1 + 平均等待时间/平均计算时间)
- 使用有界队列并设置合理的拒绝策略
8.3 并发调试工具
- JConsole/VisualVM:监控线程状态、死锁检测
- Java Mission Control:高级性能分析
- jstack:获取线程堆栈
- Thread Dump Analyzer (TDA):可视化分析线程转储
- AspectJ:编织并发调试逻辑
java复制@Aspect public class LockTracingAspect { @Before("call(* java.util.concurrent.locks.Lock.lock())") public void beforeLock() { System.out.println("Acquiring lock at " + System.currentTimeMillis()); } }
在实际项目中,我曾通过结合jstack和自定义标记日志,定位过一个由于锁粒度太粗导致的性能瓶颈。关键是要在系统设计时就加入足够的监控点,而不是等问题发生后再补救。
