本期内容为自己总结归档,共分6章,本人遇到过的面试问题会重点标记。
(若有任何疑问,可在评论区告诉我,看到就回复)
第三章:锁机制
3.1 锁的分类体系
3.1.1 锁的多维度分类全景
3.2 互斥锁(Mutex)的实现原理与性能分析
3.2.1 synchronized的底层实现与锁升级机制
synchronized是Java原生的隐式锁机制,基于JVM实现,通过对象头中的Mark Word和Monitor对象实现线程同步
其核心实现原理如下:
对象头结构与Monitor锁: Java对象的对象头包含Mark Word(存储对象的锁状态、哈希码、GC年龄等信息)和类元数据指针。Mark Word的结构会随着锁状态的变化而更新,具体包括:
| 锁状态 | Mark Word结构 | 特点 |
|---|---|---|
| 无锁 | 哈希码、分代年龄等 | 默认状态,无同步开销 |
| 偏向锁 | 线程ID、偏向模式标志 | 单线程访问时无需竞争,通过CAS直接记录线程ID |
| 轻量级锁 | 指向栈帧中Monitor Record的指针 | 多线程交替访问时,通过自旋尝试获取锁,避免OS调度 |
| 重量级锁 | 指向操作系统Monitor对象的指针 | 多线程激烈竞争时,通过OS互斥量实现线程阻塞与唤醒 |
锁升级流程: synchronized的锁升级是按需逐步升级的机制,目的是"按需升级锁粒度,减少锁竞争开销" 。具体流程如下:

锁升级的源码实现: 通过JDK8的源码片段,我们可以看到偏向锁和轻量级锁的实现逻辑:
java
// 偏向锁获取
if (ThreadLocalRandom.current().nextInt(4) != 0) {
// 通过CAS尝试将Mark Word中的线程ID设置为当前线程
if (CAS Mark Word) {
// 成功,进入偏向锁状态
} else {
// 失败,升级为轻量级锁
}
}
// 轻量级锁获取
// 将栈帧中的Monitor Record指针写入对象头的Mark Word
// 通过循环CAS尝试获取锁
while (!CAS Mark Word) {
// 自旋等待
}
3.2.2 ReentrantLock的AQS实现原理
ReentrantLock是Java显式锁的典型实现,基于**AQS(AbstractQueuedSynchronizer)**框架实现
java
// ReentrantLock的AQS实现
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
static abstract class Sync extends AbstractQueuedSynchronizer {
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
protected final boolean tryAcquire(int acquires) {
// 非公平锁实现
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (current != getExclusiveOwnerThread()) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入逻辑
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
void unlock() {
release(1);
}
}
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
state变量与可重入性: 在ReentrantLock中,state变量是一个32位的int类型,其中:
- 低16位表示锁的持有次数
- 高16位保留给其他用途
当线程第一次获取锁时,state被设置为1;当同一线程再次获取锁时,state递增;当释放锁时,state递减,直到为0时锁被完全释放。这种设计使得ReentrantLock支持可重入性,即同一线程可以多次获取同一把锁而不导致死锁。
公平锁与非公平锁的实现差异: ReentrantLock提供了公平和非公平两种锁策略,通过构造函数参数控制
java
// 非公平锁实现
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1);
}
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 公平锁实现
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁会检查队列中是否有更早等待的线程
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入逻辑
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
}
hasQueuedPredecessors()方法: 这是公平锁的关键方法,用于检查当前线程是否在等待队列的队首:
java
// AQS中的hasQueuedPredecessors()方法
public final boolean hasQueuedPredecessors() {
// 获取等待队列中的第一个节点
Node first = getFirstWaiter();
// 如果队列为空,或者当前线程是队列的第一个等待者,则没有前驱节点
return (first != null && first.nextWaiter != null) || isHeldExclusively();
}
这一方法确保了公平锁的线程获取顺序遵循FIFO原则,但也会增加一定的性能开销 。这可以防止线程"饥饿"(即某个线程长期得不到锁),但会牺牲一定的吞吐量。
3.3 读写锁(ReadWriteLock)的工作原理与适用场景
3.3.1 场景
读写锁 最适合读多写少的场景,如配置中心、缓存系统、日志记录等。例如,在一个缓存系统中,大部分时间是业务线程在读取缓存数据,只有少数时间是管理员修改配置,这时使用读写锁可以显著提升并发性能。
不适用场景 : 读写锁不适合写多读少 或读写均衡 的场景,因为此时读写锁的状态管理开销会超过其带来的性能收益。此外,读写锁也不适合强一致性要求的场景,因为并发读可能导致数据状态不一致。
3.2.2 ReentrantReadWriteLock的AQS实现
ReentrantReadWriteLock是Java中读写锁的典型实现,基于AQS框架实现,但与ReentrantLock不同的是,它将state变量分为两部分:高16位表示写锁状态,低16位表示读锁状态
读锁与写锁的获取机制: 读锁和写锁的获取通过不同的AQS子类实现:
java
// ReentrantReadWriteLock的读锁实现
public class ReadLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
}
// ReentrantReadWriteLock的写锁实现
public class WriteLock implements Lock {
private final Sync sync;
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
state变量的位操作: 读写锁通过位操作管理读锁和写锁的状态:
java
// 读锁获取
int c = getState();
if (c != 0) {
// 检查是否有写锁被持有(高16位不为0)
if (exclusiveCount(c) != 0) {
// 如果有写锁被持有,则无法获取读锁
return false;
}
}
// 增加读锁计数(低16位)
int nextc = c + SHARED;
setState(nextc);
return true;
写锁获取: 写锁获取更为严格,需要确保state为0(即没有读锁或写锁被持有):
java
int c = 0;
// 检查是否有读锁或写锁被持有
if (c == 0) {
// 公平锁需要检查队列中的等待线程
if (!fair || !hasQueuedPredecessors()) {
// 通过CAS尝试获取写锁
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
}
// 如果无法直接获取,进入等待队列
return false;
3.4 公平锁与非公平锁
公平锁和非公平锁的核心区别在于线程获取锁的顺序策略:
非公平锁:新线程尝试获取锁时,不管等待队列中是否有其他线程在排队,它都会先尝试直接获取。如果此时锁恰好可用,它就能立即获取到,而不用排队。这在大多数情况下可以提高吞吐量,减少线程切换的开销。
公平锁:内部维护了一个等待队列。当锁被释放时,锁会优先分配给在队列中等待时间最长的线程(即队首的线程),严格按照FIFO(先进先出)顺序进行。这可以防止线程"饥饿"(即某个线程长期得不到锁),但会牺牲一定的吞吐量。
3.5 乐观锁与悲观锁的实现原理与适用场景
3.5.1 乐观锁
乐观锁是一种"先执行,后检查"的并发控制策略,它假设在大多数情况下线程不会发生冲突。乐观锁的典型实现基于CAS(Compare-And-Swap)操作,如Java中的Atomic类。
CAS操作原理: CAS是一种无锁算法,它并不会为对象加锁,而是在执行的时候,看看当前数据的值是不是我们预期的那样,如果是,那就正常进行替换,如果不是,那么就替换失败 。CAS操作接收三个输入参数:一个内存地址、期望值和新值。它然后原子地比较内存地址内容与期望值,如果相等,则替换为新值;否则,不做任何修改。
AtomicInteger的CAS实现:
java
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
乐观锁的局限性:
- ABA问题:线程A读取变量值为A,线程B将其修改为B再改回A,线程A的CAS操作可能成功,但实际上变量状态已经变化 。
- 性能问题:在高竞争场景下,CAS可能导致大量自旋,降低系统性能 。
3.5.2 悲观锁
悲观锁是一种"先检查,后执行"的并发控制策略,它假设在大多数情况下线程会发生冲突。悲观锁的典型实现是synchronized和ReentrantLock 。
悲观锁的性能开销: 悲观锁的加锁、释放锁以及线程的挂起、唤醒会带来显著的性能开销 。但在高冲突场景下,悲观锁的性能反而优于乐观锁,因为它避免了频繁的更新失败和重试。
适用场景:
- 乐观锁适用场景:读多写少的场景,如多数互联网应用的信息缓存更新、商品详情查询 。
- 悲观锁适用场景:数据一致性要求极高、写操作非常频繁且冲突概率大的场景,如银行核心系统的账户余额扣减、库存管理中的出库入库操作
3.6 面试高频考点
1. synchronized和ReentrantLock的区别与选择
核心区别:
bash
// 主要差异对比表
/*
| 特性 | synchronized | ReentrantLock |
|------|-------------|---------------|
| 实现层面 | JVM内置 | JDK实现 |
| 锁释放 | 自动释放 | 必须手动unlock() |
| 可中断 | 不支持 | lockInterruptibly()支持 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify | 支持多个Condition |
| 性能 | 优化后接近 | 高竞争时更好 |
| 锁绑定 | 与对象绑定 | 灵活绑定 |
| 锁信息 | 有限信息 | 丰富的监控信息 |
*/
选择策略:
-
优先synchronized:简单同步场景,JVM会优化
-
选择ReentrantLock:需要可中断、超时、公平锁、多个条件变量
-
性能考虑:低竞争选synchronized,高竞争测试两者性能
2. 解释锁升级过程(偏向锁→轻量级锁→重量级锁)
锁升级流程: 同 3.2.1

每个阶段的原理:
-
偏向锁:记录线程ID,同一个线程重入无需CAS
-
轻量级锁:栈帧中创建Lock Record,CAS更新Mark Word
-
重量级锁:创建Monitor对象,线程进入阻塞队列
升级触发条件:
-
偏向锁撤销:其他线程尝试获取锁
-
升级为重量级锁:自旋超过阈值(-XX:PreBlockSpin)或第三个线程竞争
3. 谈谈你对AQS的理解
这块单独特别篇
