一、synchronized 与锁升级
Q1:synchronized 的三种使用方式是什么?
答:
// 1. 修饰实例方法:锁当前对象 this
public synchronized void method() {}
// 2. 修饰静态方法:锁当前 Class 对象
public static synchronized void staticMethod() {}
// 3. 修饰代码块:锁指定对象
public void block() {
synchronized (lock) { }
}本质:synchronized 锁的是对象,不是代码。
Q2:synchronized 的底层原理是什么?
答: 通过 monitorenter / monitorexit 字节码指令实现:
synchronized (lock) {
// 代码
}编译后:
monitorenter
// 代码
monitorexit每个对象关联一个 Monitor(管程)
monitorenter:尝试获取 Monitor 所有权monitorexit:释放 Monitor异常时,编译器会自动插入
monitorexit(保证释放)
方法级 synchronized 通过方法访问标志 ACC_SYNCHRONIZED 实现,原理相同。
Q3:什么是 Monitor?它的结构是什么?
答: Monitor 是 JVM 实现的同步机制,对应 C++ 的 ObjectMonitor:
工作流程:
线程进入
_EntryList竞争锁获取成功,
_owner指向自己,_count++调用
wait(),进入_WaitSet释放锁,
_owner = null,唤醒_EntryList线程
Q4:synchronized 的锁升级过程是什么?
答: 锁状态从低到高:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁升级过程:
偏向锁:只有一个线程访问,Mark Word 记录线程 ID,无竞争
轻量级锁:有第二个线程竞争,升级为轻量级锁,用 CAS 自旋
重量级锁:自旋失败或竞争激烈,升级为重量级锁,线程阻塞
关键:锁升级是单向的,不可降级(HotSpot 中偏向锁可批量撤销,但整体趋势单向)。
Q5:什么是偏向锁?为什么需要它?
答: 偏向锁针对只有一个线程访问同步块的场景优化:
第一次获取锁时,把线程 ID 写入对象头的 Mark Word
后续同一线程进入,只需检查线程 ID,无需 CAS
几乎无开销
// 对象头 Mark Word(64 位)
// 偏向锁:| 线程ID(54) | Epoch(2) | 未使用(1) | 分代年龄(4) | 偏向标志(1) | 锁标志(2) |优点:单线程场景下,避免 CAS 开销。
注意:JDK 15 后偏向锁默认禁用(维护成本高,收益有限)。
Q6:什么是轻量级锁?它的原理是什么?
答: 轻量级锁针对少量线程竞争、锁持有时间短的场景:
线程在栈帧中创建 Lock Record
用 CAS 把对象头的 Mark Word 替换为指向 Lock Record 的指针
成功则获取锁;失败则自旋重试
自旋失败,升级为重量级锁
// 轻量级锁 Mark Word
// | 指向栈中锁记录的指针(62) | 锁标志(2) = 00 |优点:避免线程阻塞和上下文切换。
缺点:自旋消耗 CPU,竞争激烈时反而更慢。
Q7:什么是自旋锁?自适应自旋是什么?
答:
自旋锁:不立即阻塞,而是循环等待(CAS),避免上下文切换
自适应自旋:JVM 根据历史成功率动态调整自旋次数
// 伪代码
while (!cas(lock, unlocked, locked)) {
// 自旋
}自适应策略:
上次自旋成功 → 允许更多自旋
上次自旋失败 → 减少甚至跳过自旋
JDK 6 后默认开启自适应自旋。
Q8:什么是重量级锁?
答: 竞争激烈时,锁升级为重量级锁:
依赖操作系统 Mutex Lock
未获取锁的线程阻塞,进入
_EntryList涉及用户态 → 内核态切换,开销大
特点:
不会空转 CPU
但上下文切换成本高
适用:锁持有时间长、竞争激烈的场景。
Q9:锁升级的优缺点是什么?
答:
核心思想:根据竞争程度,逐级升级,平衡开销。
Q10:synchronized 和 ReentrantLock 的区别?
答:
选择:简单场景用 synchronized,需要高级功能用 ReentrantLock。
二、ReentrantLock
Q11:什么是可重入锁?ReentrantLock 如何实现可重入?
答: 可重入锁指同一线程可以多次获取同一把锁:
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock(); // 可重入
lock.unlock();
lock.unlock();实现:
AQS 的
state字段记录重入次数每次
lock():state++每次
unlock():state--,减到 0 才释放
// ReentrantLock 非公平锁
final boolean nonfairTryAcquire(int acquires) {
if (state == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(state + acquires); // 重入
return true;
}
return false;
}Q12:ReentrantLock 的公平锁和非公平锁有什么区别?
答:
ReentrantLock fair = new ReentrantLock(true); // 公平
ReentrantLock unfair = new ReentrantLock(false); // 非公平(默认)非公平锁:新线程直接 CAS 抢锁,抢不到才排队,减少唤醒开销。
Q13:ReentrantLock 的 tryLock() 有什么用?
答: 尝试获取锁,不阻塞:
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { // 立即返回
try { /* 业务 */ }
finally { lock.unlock(); }
} else {
// 获取失败,做其他事
}
// 带超时
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { /* 业务 */ }
finally { lock.unlock(); }
}用途:避免死锁、定时任务、降级处理。
Q14:lockInterruptibly() 和 lock() 有什么区别?
答:
lock():不可中断,一直等待lockInterruptibly():可被interrupt()中断
lock.lockInterruptibly();
try {
// 若被中断,抛 InterruptedException
} finally {
lock.unlock();
}用途:避免线程无限等待,支持取消操作。
Q15:ReentrantLock 必须放在 try-finally 中吗?
答: 必须,否则异常时锁不释放,导致死锁:
// 错误
lock.lock();
doSomething(); // 抛异常,锁永远不释放
lock.unlock();
// 正确
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}注意:lock() 应在 try 之前,避免 lock 失败后 unlock。
三、AQS 原理
Q16:什么是 AQS?
答: AQS(AbstractQueuedSynchronizer)是 JDK 并发包的基础框架:
核心:一个 volatile int state + CLH 双向队列
子类只需实现
tryAcquire/tryRelease等方法基于 AQS 的组件:ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private transient volatile Node head; // 队列头
private transient volatile Node tail; // 队列尾
}Q17:AQS 的核心结构是什么?
答:
state:volatile int,表示同步状态
ReentrantLock:0 未锁,>0 重入次数
Semaphore:剩余许可数
CountDownLatch:剩余计数
CLH 队列:双向链表,存放等待线程
每个节点是
Node,包含线程引用和等待状态FIFO 顺序
Condition 队列:条件等待队列
head → node1 → node2 → ... → tailQ18:AQS 的独占模式和共享模式有什么区别?
答:
独占:同一时刻只有一个线程持有
共享:多个线程可同时持有
// 独占:state 0→1
// 共享:state 表示可用许可数,可多个线程同时减Q19:AQS 的 acquire 流程是什么?
答: 以 ReentrantLock 为例:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 2. 失败则入队,自旋/阻塞
selfInterrupt();
}
}流程:
tryAcquire:尝试 CAS 修改 state失败:创建 Node,加入 CLH 队列尾部
acquireQueued:自旋检查前驱是否为 head,是则尝试获取仍失败:
LockSupport.park()阻塞前驱释放锁时
unpark唤醒
Q20:AQS 的 release 流程是什么?
答:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 1. 尝试释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 2. 唤醒后继节点
return true;
}
return false;
}流程:
tryRelease:state 减到 0,设置 owner 为 null唤醒 head 的后继节点
被唤醒线程重新竞争锁
Q21:AQS 中为什么要用自旋 + 阻塞?
答:
纯自旋:CPU 空转,浪费资源
纯阻塞:上下文切换开销大
AQS 策略:
先自旋几次,尝试获取锁(快速路径)
失败则
park()阻塞(慢速路径)被唤醒后再自旋竞争
平衡:短等待用自旋,长等待用阻塞。
Q22:AQS 的 Node 有哪些状态?
答: waitStatus:
SIGNAL:当前节点释放锁时,必须唤醒后继节点。
Q23:为什么 AQS 用双向队列而不是单向?
答: 因为需要从尾部找有效前驱:
节点取消时,需要向前查找
unparkSuccessor从尾部往前找,避免遍历整个队列双向队列支持 O(1) 尾部插入和高效前向查找
// 从尾部往前找有效节点
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;
}Q24:AQS 如何实现公平锁和非公平锁?
答:
非公平锁:直接 CAS 抢,不看队列
final boolean nonfairTryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// ...
}公平锁:先检查是否有前驱
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors()
&& compareAndSetState(0, acquires)) {
// 只有队列为空才抢
}
}区别:公平锁多一个 hasQueuedPredecessors() 检查。
四、读写锁
Q25:什么是读写锁?解决什么问题?
答: ReentrantReadWriteLock 分离读锁和写锁:
读锁(共享):多个线程可同时读
写锁(独占):只有一个线程可写
读写互斥、写写互斥、读读不互斥
解决问题:读多写少场景下,synchronized 让读也串行,性能差。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadLock readLock = rwLock.readLock();
WriteLock writeLock = rwLock.writeLock();
readLock.lock(); // 共享
readLock.unlock();
writeLock.lock(); // 独占
writeLock.unlock();Q26:读写锁的规则是什么?
答:
总结:读读共享,其他互斥。
Q27:读写锁如何用 AQS 实现?
答: 用 state 的高 16 位表示读锁,低 16 位表示写锁:
// state 高16位:读锁持有数
// state 低16位:写锁重入次数
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << 16) - 1;
// 读锁数量
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
// 写锁数量
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}读锁:tryAcquireShared,CAS 增加高 16 位
写锁:tryAcquire,CAS 增加低 16 位
Q28:读写锁支持锁降级吗?什么是锁降级?
答: 支持写锁降级为读锁,不支持读锁升级为写锁。
// 锁降级
writeLock.lock();
try {
// 修改数据
readLock.lock(); // 获取读锁
} finally {
writeLock.unlock(); // 释放写锁,仍持有读锁
}
try {
// 读数据
} finally {
readLock.unlock();
}为什么支持降级:保证数据可见性和一致性。
为什么不支持升级:多个读锁同时升级会死锁。
Q29:什么是锁饥饿?读写锁如何避免写饥饿?
答: 读多写少时,读锁可能一直占用,写锁永远等不到 → 写饥饿。
ReentrantReadWriteLock 策略:
非公平模式:读锁可插队,可能写饥饿
公平模式:FIFO,避免饥饿
写锁请求会阻塞后续读锁(非公平下也有一定保护)
ReentrantReadWriteLock fair =
new ReentrantReadWriteLock(true);Q30:StampedLock 是什么?和读写锁有什么区别?
答: StampedLock 是 JDK 8 引入的乐观读锁:
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 乐观读
int data = readData();
if (!lock.validate(stamp)) { // 检查是否被修改
stamp = lock.readLock(); // 降级为悲观读
try { data = readData(); }
finally { lock.unlockRead(stamp); }
}乐观读:不加锁,读完检查版本号,适合读多写极少。
五、CAS 与原子类
Q31:什么是 CAS?
答: CAS(Compare And Swap)是一种无锁原子操作:
// 伪代码
boolean cas(int expected, int newValue) {
if (value == expected) {
value = newValue;
return true;
}
return false;
}三个操作数:
V:内存值
A:预期值
B:新值
当且仅当 V == A 时,V = B,否则不做任何操作。
底层:CPU 的 cmpxchg 指令,保证原子性。
Q32:CAS 有什么问题?
答: 三大问题:
ABA 问题
值从 A → B → A,CAS 无法感知
解决:加版本号(
AtomicStampedReference)
自旋开销
竞争激烈时,长时间自旋耗 CPU
解决:自适应自旋、限制自旋次数
只能保证一个变量原子性
多个变量需用锁或
AtomicReference
// ABA 问题示例
AtomicInteger ai = new AtomicInteger(1);
// 线程1:读到1,准备 CAS 为 2
// 线程2:1→2→1
// 线程1:CAS(1, 2) 成功,但中间已变过Q33:如何解决 ABA 问题?
答: 用版本号:
AtomicStampedReference<Integer> ref =
new AtomicStampedReference<>(1, 0);
int stamp = ref.getStamp();
ref.compareAndSet(1, 2, stamp, stamp + 1);原理:每次修改版本号 +1,CAS 同时比较值和版本号。
AtomicMarkableReference 用 boolean 标记,类似。
Q34:原子类有哪些?分类是什么?
答: JDK 提供 java.util.concurrent.atomic 包:
Q35:AtomicInteger 的底层原理是什么?
答: 基于 CAS + volatile:
public class AtomicInteger {
private volatile int value;
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
// Unsafe 方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
}value用 volatile 保证可见性Unsafe.compareAndSwapInt保证原子性失败则自旋重试
Q36:LongAdder 和 AtomicLong 有什么区别?
答:
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum(); // 累加所有 CellLongAdder 核心:把热点分散到多个 Cell,减少 CAS 冲突。
Q37:LongAdder 的分段思想是什么?
答:
维护一个
base变量 +Cell[]数组无竞争:直接 CAS base
有竞争:哈希到某个 Cell,CAS 该 Cell
求和:base + 所有 Cell 的值
// 伪代码
public void increment() {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null ||
!casBase(b = base, b + 1)) {
// 竞争激烈,使用 Cell
int index = getProbe() & (as.length - 1);
casCell(as[index], 1);
}
}思想:空间换时间,分散热点。
Q38:CAS 和 synchronized 如何选择?
答:
核心:CAS 适合简单、低竞争;锁适合复杂、高竞争。
Q39:什么是乐观锁和悲观锁?
答:
乐观锁:CAS、数据库 version 字段
悲观锁:synchronized、SELECT ... FOR UPDATE
Q40:AtomicReference 如何使用?
答:
AtomicReference<String> ref = new AtomicReference<>("A");
// CAS 更新
boolean success = ref.compareAndSet("A", "B");
// 获取并更新
String old = ref.getAndSet("C");
// 函数式更新
ref.updateAndGet(s -> s + "!");用途:原子更新对象引用,实现无锁数据结构。
六、综合与进阶
Q41:synchronized 锁升级和 AQS 有什么关系?
答: 两者是不同的锁实现:
synchronized:JVM 层,Monitor + 锁升级
AQS:JDK 层,state + CLH 队列
联系:
都基于 CAS
都涉及自旋和阻塞
synchronized 由 JVM 优化,AQS 由开发者控制
选择:简单用 synchronized,复杂用 AQS 系(ReentrantLock 等)。
Q42:ReentrantLock 的 lock() 和 lockInterruptibly() 底层区别?
答:
lock():acquire(1),不可中断lockInterruptibly():acquireInterruptibly(1),可中断
// 可中断版本
public final void acquireInterruptibly(int arg) {
if (Thread.interrupted()) throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg); // 阻塞时检查中断
}区别:可中断版本在 park 时响应中断,抛异常。
Q43:AQS 的 Condition 如何实现?
答: Condition 内部维护等待队列:
await():释放锁,加入等待队列,parksignal():把等待队列头节点移到同步队列被唤醒后重新竞争锁
await: 同步队列 → 等待队列
signal: 等待队列 → 同步队列核心:两个队列之间的节点转移。
Q44:如何实现一个自定义同步器?
答: 继承 AQS,实现 tryAcquire/tryRelease:
class Mutex extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(
Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public void unlock() { release(1); }
}要点:只需实现 tryAcquire/tryRelease,其余由 AQS 处理。
Q45:为什么 AQS 的 state 用 volatile?
答: 保证可见性:
一个线程修改 state,其他线程立即可见
配合 CAS 保证原子性
volatile + CAS = 无锁并发的基石
private volatile int state;注意:volatile 只保证可见性,原子性由 CAS 保证。
评论