面试官:"请说一下Java内存模型,volatile能保证原子性吗?synchronized的锁升级过程是怎样的?AQS的原理是什么?线程池的核心参数怎么设置?"
你:(从容地翻开笔记本,从内存模型开始层层递进...)
考察重点
这道题是中高级Java面试的必考重难点,面试官想考察:
- 底层理解:对Java内存模型、CPU缓存一致性、指令重排序的理解深度
- 原理掌握:synchronized的优化历程、AQS的设计思想
- 实战能力:线程池参数设置、并发工具类的正确使用
- 源码功底:是否读过AQS、ReentrantLock、ThreadPoolExecutor源码
一、Java内存模型(JMM)与volatile
1.1 为什么需要Java内存模型?
硬件层面的问题:CPU和内存之间速度差异巨大,引入了CPU缓存(L1/L2/L3)、写缓冲器、无效化队列等硬件优化,导致可见性问题和有序性问题。
Java内存模型(JMM):屏蔽不同硬件和操作系统的内存访问差异,保证Java程序在各种平台下都能达到一致的内存访问效果。
JMM的核心概念:
- 主内存:所有变量都存储在主内存中(所有线程共享)
- 工作内存:每个线程有自己的工作内存,保存了被该线程使用的变量的主内存副本拷贝
- 线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Thread1 │ │ Thread2 │ │ Thread3 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 工作内存 │ │ 工作内存 │ │ 工作内存 │
│ 变量副本 │ │ 变量副本 │ │ 变量副本 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────────────────┼──────────────────┘
▼
┌─────────────────┐
│ 主内存 │
│ 共享变量 │
└─────────────────┘
1.2 volatile的内存语义
核心作用:
- 保证可见性:对volatile变量的写操作会立即刷新到主内存,读操作会从主内存读取最新值
- 禁止指令重排序:在volatile变量前后插入内存屏障,防止编译器、CPU对指令重排序
volatile写-读建立的happens-before关系:
- 对一个volatile变量的写操作,happens-before于后续对这个volatile变量的读操作
- 这意味着:线程A写volatile变量,线程B读同一个volatile变量,那么线程A在写之前的所有操作的结果对线程B都是可见的
1.3 volatile的实现原理
字节码层面:
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true; // 字节码: putstatic 字段
}
}
JVM层面:编译成字节码后,JVM会在volatile变量操作时添加内存屏障(Memory Barrier)
CPU层面:在x86架构下,volatile写操作会在硬件层面通过MESI缓存一致性协议 + Lock前缀指令实现。
x86下的volatile写实现(lock addl $0x0, (%rsp)):
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(通过总线嗅探机制)
内存屏障策略(JMM规范):
| 屏障类型 |
指令示例 |
说明 |
| LoadLoad |
Load1; LoadLoad; Load2 |
确保Load1的数据加载先于Load2 |
| StoreStore |
Store1; StoreStore; Store2 |
确保Store1的数据刷入内存先于Store2 |
| LoadStore |
Load1; LoadStore; Store2 |
确保Load1的数据加载先于Store2写入 |
| StoreLoad |
Store1; StoreLoad; Load2 |
确保Store1的数据刷入内存先于Load2加载(开销最大) |
volatile读/写插入的屏障:
- volatile读:LoadLoad屏障(后续读不能重排到前面)
- volatile写:StoreStore屏障(前面写不能重排到后面)+ StoreLoad屏障(防止写和后续读重排)
1.4 volatile能保证原子性吗?
不能! volatile只能保证单次读/写的原子性(对基本类型的读写是原子性的),但不能保证复合操作的原子性。
public class VolatileAtomicTest {
private volatile int count = 0;
public void increment() {
count++; // 非原子操作:读-改-写
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicTest test = new VolatileAtomicTest();
for (int i = 0; i < 1000; i++) {
new Thread(test::increment).start();
}
Thread.sleep(3000);
System.out.println(test.count); // 结果可能小于1000
}
}
1. 从内存读取count到寄存器(read)
2. 寄存器中+1操作(modify)
3. 将值写回内存(write)
解决方案:使用synchronized、ReentrantLock或AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // CAS保证原子性
}
1.5 volatile vs synchronized
| 特性 |
volatile |
synchronized |
| 可见性 |
✅ 保证 |
✅ 保证 |
| 原子性 |
❌ 不保证(仅单次读写) |
✅ 保证 |
| 互斥性 |
❌ 无 |
✅ 互斥锁 |
| 阻塞性 |
非阻塞 |
阻塞 |
| 性能 |
轻量级(无锁) |
重量级(锁竞争时) |
| 适用场景 |
单一变量的可见性控制 |
复合操作、代码块互斥 |
二、synchronized的锁升级过程
2.1 锁的状态
JDK 1.6对synchronized进行了大量优化,引入了锁升级机制,锁的状态从低到高分为四种(无锁、偏向锁、轻量级锁、重量级锁),锁只能升级,不能降级。
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
2.2 对象头结构
锁状态信息存储在Java对象的对象头(Mark Word)中。64位JVM的Mark Word结构:
| 锁状态 |
25bit |
31bit |
1bit |
4bit |
1bit |
2bit |
| 无锁 |
unused |
hashCode |
0 |
分代年龄 |
0 |
01 |
| 偏向锁 |
threadId(54bit) |
epoch(2bit) |
1 |
分代年龄 |
0 |
01 |
| 轻量级锁 |
指向栈中锁记录的指针(62bit) |
|
|
00 |
|
|
| 重量级锁 |
指向互斥量(重量级锁)的指针(62bit) |
|
|
10 |
|
|
| GC标记 |
空 |
|
|
11 |
|
|
2.3 偏向锁
场景:锁总是由同一个线程多次获取(如Vector、StringBuffer等线程安全类)
原理:当锁对象第一次被线程获取时,JVM将对象头的Mark Word设置为偏向模式,记录线程ID。之后该线程再次进入同步块时,只需比较线程ID,无需CAS操作。
偏向锁撤销:当有其他线程尝试竞争锁时,偏向锁升级为轻量级锁。
JVM参数:
# 开启偏向锁(JDK6默认开启,JDK15后默认关闭)
-XX:+UseBiasedLocking
# 偏向锁启动延迟(默认4秒)
-XX:BiasedLockingStartupDelay=0
2.4 轻量级锁
场景:多个线程交替执行,没有真正的竞争
原理:
- 线程A进入同步块,JVM在当前线程的栈帧中创建锁记录(Lock Record)
- 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)
- 使用CAS操作将对象头的Mark Word替换为指向锁记录的指针
- 如果CAS成功,线程A获得锁;如果失败,说明有竞争,膨胀为重量级锁
轻量级锁解锁:使用CAS将Displaced Mark Word替换回对象头
2.5 重量级锁
场景:多个线程同时竞争锁
原理:通过操作系统的互斥量(mutex)实现,线程阻塞和唤醒需要从用户态切换到内核态,开销较大。
内部实现:依赖于monitor对象(每个Java对象都有一个monitor),使用monitorenter和monitorexit指令。
2.6 锁升级流程图
┌─────────────────────────────────────────────────────────────┐
│ 无锁状态 │
└────────────────────────────┬────────────────────────────────┘
│ 第一个线程进入同步块
▼
┌─────────────────────────────────────────────────────────────┐
│ 偏向锁状态 │
│ - 记录线程ID到对象头 │
│ - 后续同线程进入无CAS操作 │
└────────────────────────────┬────────────────────────────────┘
│ 其他线程尝试获取锁
▼
┌─────────────────────────────────────────────────────────────┐
│ 轻量级锁状态 │
│ - CAS尝试获取锁,失败则自旋 │
│ - 自旋次数限制(默认10次) │
└────────────────────────────┬────────────────────────────────┘
│ 自旋超过限制/有线程自旋等待
▼
┌─────────────────────────────────────────────────────────────┐
│ 重量级锁状态 │
│ - 线程阻塞,进入等待队列 │
│ - 依赖操作系统mutex │
└─────────────────────────────────────────────────────────────┘
2.7 锁优化的其他手段
自旋锁(Spin Lock):线程不会立即阻塞,而是执行一段忙循环等待锁释放。适用于锁持有时间短的场景。
自适应自旋:JDK6引入,自旋次数根据上一次在同一锁上的自旋时间动态调整。
锁消除(Lock Elimination):JIT编译器检测到不存在竞争时,消除锁。如StringBuffer作为局部变量时。
锁粗化(Lock Coarsening):将多个连续的加锁解锁操作合并为一次,减少锁操作次数。
三、AQS框架源码深度剖析
3.1 什么是AQS?
AbstractQueuedSynchronizer(AQS) 是Java并发包(JUC)的核心基础框架,它提供了一个FIFO队列和同步状态管理的模板,ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock等都是基于AQS实现的。
核心设计思想:
- 同步状态(state):用volatile int表示,通过
getState()、setState()、compareAndSetState()操作
- CLH队列:基于双向链表的FIFO等待队列,存放等待获取锁的线程节点
- 模板方法模式:子类只需实现
tryAcquire()、tryRelease()等方法
3.2 AQS核心数据结构
// AQS中的Node(等待队列节点)
static final class Node {
// 节点状态
static final int CANCELLED = 1; // 取消
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下,传播释放
volatile int waitStatus; // 等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前节点持有的线程
Node nextWaiter; // 条件队列的下一个节点
}
// AQS核心字段
public abstract class AbstractQueuedSynchronizer {
private transient volatile Node head; // 队列头节点
private transient volatile Node tail; // 队列尾节点
private volatile int state; // 同步状态
}
3.3 独占模式(以ReentrantLock为例)
获取锁的流程:
// 入口:ReentrantLock.lock()
public void lock() {
sync.acquire(1);
}
// AQS.acquire()
public final void acquire(int arg) {
// 1. tryAcquire:尝试获取锁(子类实现)
// 2. addWaiter:失败则加入等待队列
// 3. acquireQueued:在队列中自旋获取锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 创建节点并加入队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 初始化队列或CAS失败时自旋入队
return node;
}
// 在队列中自旋获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 只有前驱是头节点时才尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 设置当前节点为头节点
p.next = null;
failed = false;
return interrupted;
}
// 检查是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放锁的流程:
// 入口:ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
// AQS.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
// 唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果后继节点被取消或为空,从尾部向前找第一个有效节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
3.4 共享模式(以CountDownLatch为例)
共享模式的核心区别:多个线程可以同时获取同步状态。
// CountDownLatch的await()
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) {
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取共享锁
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg) {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 设置头并传播
p.next = null;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
// CountDownLatch的countDown()
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 唤醒所有等待线程
return true;
}
return false;
}
3.5 Condition实现原理
Condition基于AQS的条件队列实现,每个Condition对象对应一个条件队列。
// await()流程
public final void await() throws InterruptedException {
// 1. 创建节点加入条件队列
Node node = addConditionWaiter();
// 2. 释放锁(完全释放)
int savedState = fullyRelease(node);
// 3. 阻塞,直到被signal
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
}
// 4. 重新获取锁
acquireQueued(node, savedState);
}
// signal()流程
public final void signal() {
// 1. 从条件队列头部取出节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 2. 从条件队列移除
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 3. 转移到同步队列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
四、线程池的核心参数和拒绝策略
4.1 线程池核心参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) // 拒绝策略
| 参数详解: |
参数 |
作用 |
设置建议 |
| corePoolSize |
核心线程数,即使空闲也不会回收 |
CPU密集型:CPU核心数+1 IO密集型:CPU核心数×2 |
| maximumPoolSize |
最大线程数 |
队列满时才创建额外线程 |
| keepAliveTime |
非核心线程空闲存活时间 |
根据业务场景(30s~5min) |
| workQueue |
任务队列 |
见下表 |
| threadFactory |
自定义线程创建(命名、优先级) |
必设!便于问题排查 |
| handler |
队列满且线程池满时的拒绝策略 |
根据业务场景选择 |
| 任务队列类型: |
队列类型 |
特点 |
适用场景 |
SynchronousQueue |
不存储任务,直接交给线程 |
无界线程池(CachedThreadPool) |
LinkedBlockingQueue |
无界队列(默认容量Integer.MAX) |
任务量可控,避免OOM |
ArrayBlockingQueue |
有界队列 |
任务量可控,需要限制积压 |
DelayQueue |
延迟任务队列 |
定时/延迟任务 |
4.2 线程池执行流程
提交任务
│
▼
当前线程数 < corePoolSize?
│
├── 是 ──→ 创建新线程执行
│
└── 否 ──→ 任务队列是否已满?
│
├── 否 ──→ 加入队列等待
│
└── 是 ──→ 当前线程数 < maximumPoolSize?
│
├── 是 ──→ 创建新线程执行
│
└── 否 ──→ 执行拒绝策略
4.3 拒绝策略
| JDK内置四种拒绝策略: |
策略 |
行为 |
适用场景 |
AbortPolicy(默认) |
抛出RejectedExecutionException |
关键业务,必须处理 |
CallerRunsPolicy |
由调用线程执行任务 |
降级策略,保证任务不丢失 |
DiscardPolicy |
丢弃任务,不抛异常 |
非核心业务 |
DiscardOldestPolicy |
丢弃队列头任务,重试提交 |
追求新任务的处理 |
4.4 线程池的正确使用
常见错误:
// ❌ 错误1:使用Executors快捷方法(可能导致OOM)
ExecutorService executor = Executors.newFixedThreadPool(10);
// LinkedBlockingQueue无界,任务积压会导致OOM
// ❌ 错误2:不设置ThreadFactory
// 无法定位问题线程,线程名称无意义
// ❌ 错误3:没有优雅关闭
// 任务可能丢失或强制中断
正确使用:
// ✅ 推荐:手动创建ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60, TimeUnit.SECONDS, // 空闲存活时间
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder() // Guava的ThreadFactoryBuilder
.setNameFormat("biz-pool-%d")
.setDaemon(false)
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 降级策略
);
// 优雅关闭
executor.shutdown(); // 不再接收新任务,等待已提交任务执行
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 超时则强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
五、实战:并发工具类的正确使用姿势
5.1 并发容器选型
| 场景 |
推荐容器 |
原因 |
| 读多写少 |
CopyOnWriteArrayList |
写时复制,读无锁 |
| 单线程读+写 |
ConcurrentHashMap |
分段锁/CAS,高并发 |
| 需要排序 |
ConcurrentSkipListMap |
跳表实现,线程安全 |
| 生产者消费者 |
ArrayBlockingQueue |
有界阻塞队列 |
| 延迟任务 |
DelayQueue |
按延迟时间取出 |
5.2 线程安全工具类
// 1. AtomicInteger:CAS实现,原子操作
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子+1
// 2. LongAdder:高并发下性能优于AtomicLong
LongAdder adder = new LongAdder();
adder.add(1);
adder.sum();
// 3. CountDownLatch:等待多个任务完成
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
// 业务逻辑
latch.countDown();
});
}
latch.await(10, TimeUnit.SECONDS);
// 4. CyclicBarrier:循环屏障,多线程等待到齐后同时执行
CyclicBarrier barrier = new CyclicBarrier(5, () -> {
System.out.println("所有线程准备就绪,开始执行");
});
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
// 准备工作
barrier.await(); // 等待所有线程到齐
// 同时执行
});
}
// 5. Semaphore:限流控制
Semaphore semaphore = new Semaphore(10); // 最大并发10
try {
semaphore.acquire();
// 业务逻辑
} finally {
semaphore.release();
}
// 6. CompletableFuture:异步编排
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> queryData())
.thenApply(data -> processData(data))
.thenApply(result -> formatResult(result))
.exceptionally(ex -> "error: " + ex.getMessage());
5.3 死锁排查与预防
死锁四个必要条件:
- 互斥条件:资源不能被共享
- 请求与保持:持有一个资源并等待其他资源
- 不可剥夺:已获得资源不可强行剥夺
- 循环等待:多个线程形成循环等待链
排查方法:
# 1. 查看Java进程
jps -l
# 2. 打印线程堆栈,查找deadlock信息
jstack <pid>
# 输出示例:
# Found one Java-level deadlock:
# "Thread-0":
# waiting for ownable synchronizer 0x000000071a123456,
# held by "Thread-1"
# "Thread-1":
# waiting for ownable synchronizer 0x000000071a123457,
# held by "Thread-0"
预防策略:
- 按固定顺序获取锁
- 使用
tryLock()设置超时
- 减少锁粒度
- 使用并发容器替代手动加锁
六、思维导图总结
并发编程核心
├── Java内存模型(JMM)
│ ├── 主内存 vs 工作内存
│ ├── happens-before规则
│ └── volatile:可见性、禁止重排序
├── 锁机制
│ ├── synchronized锁升级
│ │ ├── 偏向锁
│ │ ├── 轻量级锁
│ │ └── 重量级锁
│ └── AQS框架
│ ├── state + CLH队列
│ ├── 独占模式(ReentrantLock)
│ ├── 共享模式(CountDownLatch)
│ └── Condition条件队列
├── 线程池
│ ├── 核心参数:corePoolSize、maximumPoolSize、workQueue
│ ├── 执行流程
│ └── 拒绝策略
└── 并发工具
├── 原子类:AtomicInteger、LongAdder
├── 同步工具:CountDownLatch、CyclicBarrier、Semaphore
├── 并发容器:ConcurrentHashMap、CopyOnWriteArrayList
└── 异步编程:CompletableFuture
七、面试Tips
加分话术
- 底层结合:"volatile底层是通过在汇编层面添加lock前缀指令实现的,这个指令会触发缓存一致性协议..."
- 源码引用:"从AQS源码可以看到,获取锁失败时会通过shouldParkAfterFailedAcquire判断是否需要阻塞..."
- 实战经验:"之前优化过一个高并发系统,发现
synchronized锁竞争激烈,通过锁升级分析发现是锁粒度太大..."
常见追问准备
- 什么是指令重排序?volatile如何禁止?
synchronized和ReentrantLock的区别?
- AQS为什么用双向链表?
- 线程池的核心线程数如何计算?
ThreadLocal的原理和内存泄漏问题?
避坑指南
- ❌ 不要混淆“可见性”和“原子性”
- ❌ 不要认为volatile能保证count++的原子性
- ❌ 不要忽略线程池的优雅关闭
- ✅ 强调锁升级的意义(减少重量级锁的开销)
- ✅ 区分AQS的独占模式和共享模式
理解和掌握这些并发编程的核心知识,不仅是应对面试的关键,更是构建高性能、高可用Java应用的基石。如果你对Java并发或JVM底层有更深入的兴趣,欢迎访问云栈社区与其他开发者交流探讨。