Java锁机制全解析:从AQS到CAS,深入理解synchronized与ReentrantLock
在Java并发编程中,锁是保证线程安全的核心机制。面试中,关于锁的底层实现、性能对比和适用场景的问题屡见不鲜。本文将系统解析Java中最重要的锁机制,包括AQS框架、CAS原理,以及synchronized
与ReentrantLock
的核心区别,助你全面掌握锁相关知识点。
一、并发安全的基石:锁的核心作用
在多线程环境下,多个线程同时操作共享资源可能导致数据不一致(如i++
的经典问题)。锁的核心作用是通过控制线程对共享资源的访问权限,保证同一时间只有一个(或有限个)线程能操作资源,从而避免并发安全问题。
Java中的锁机制可分为两类:
- 隐式锁:如
synchronized
,由JVM自动管理,无需手动释放。 - 显式锁:如
ReentrantLock
,需手动获取和释放,灵活性更高。
无论哪种锁,其底层都依赖两种核心技术:AQS框架和CAS操作。
二、深入理解AQS:Java锁的基础框架
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是Java并发包中许多锁和同步工具的底层基础,如ReentrantLock
、CountDownLatch
、Semaphore
等都基于AQS实现。
1. AQS的核心设计
AQS的核心思想是**“状态管理+队列等待”**,主要包含三个部分:
-
状态变量(state):一个
volatile int
变量,用于表示同步状态(如锁的持有计数)。- 例如,
ReentrantLock
中,state=0
表示锁未被持有,state>0
表示被持有(数值代表重入次数)。
- 例如,
-
双向等待队列(CLH队列):当线程获取锁失败时,会被包装成节点(Node) 加入队列尾部,进入阻塞状态等待唤醒。
- 队列采用双向链表实现,每个节点包含线程引用、等待状态(如
CANCELLED
、SIGNAL
等)。
- 队列采用双向链表实现,每个节点包含线程引用、等待状态(如
-
条件队列(Condition Queue):通过
Condition
接口实现,用于线程间的条件等待(类似Object.wait()
)。- 一个AQS可以关联多个条件队列,实现更灵活的等待/唤醒机制。
2. AQS的核心操作
AQS对外提供了模板方法,子类需重写以下核心方法(通过修改state
实现自定义同步逻辑):
tryAcquire(int arg)
:尝试获取锁(独占模式)。tryRelease(int arg)
:尝试释放锁(独占模式)。tryAcquireShared(int arg)
:尝试获取共享锁(如Semaphore
)。tryReleaseShared(int arg)
:尝试释放共享锁。
工作流程(以独占锁为例):
- 线程调用
acquire()
方法获取锁,内部先调用tryAcquire()
。 - 若
tryAcquire()
成功(state
修改成功),则直接获取锁。 - 若失败,当前线程被包装成Node加入等待队列,进入阻塞状态。
- 当锁释放时(
tryRelease()
成功),唤醒队列中的后继节点,使其重新尝试获取锁。
三、CAS:无锁编程的核心技术
CAS(Compare And Swap,比较并交换)是一种无锁原子操作,是实现乐观锁的基础,也是AQS、AtomicInteger
等并发工具的底层依赖。
1. CAS的工作原理
CAS操作包含三个参数:内存地址(V)、预期值(A)、新值(B)。
- 核心逻辑:若内存地址V中的值等于预期值A,则将其更新为B;否则不做操作。
- 操作是原子性的,由CPU指令(如
cmpxchg
)保证,无需加锁。
用伪代码表示:
boolean cas(V, A, B) {if (V的值 == A) {V = B;return true; // 操作成功}return false; // 操作失败
}
2. CAS在Java中的应用
Java通过sun.misc.Unsafe
类提供CAS操作的封装,例如AtomicInteger
的incrementAndGet()
方法:
public final int incrementAndGet() {// 循环尝试CAS,直到成功return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}// Unsafe类中的CAS实现
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset); // 获取当前值(保证可见性)} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS尝试更新return v;
}
3. CAS的优缺点
- 优点:无锁操作,避免线程切换和阻塞的开销,在低并发场景下性能优于锁。
- 缺点:
- ABA问题:若值从A变为B再变回A,CAS会误认为未修改(可通过版本号解决,如
AtomicStampedReference
)。 - 自旋开销:高并发下CAS可能多次失败重试,导致CPU空转。
- 只能保证单个变量的原子性:无法直接实现多变量的原子操作。
- ABA问题:若值从A变为B再变回A,CAS会误认为未修改(可通过版本号解决,如
四、synchronized:JVM内置锁的进化之路
synchronized
是Java最基本的同步机制,从JDK 1.0就存在,经过多年优化(如JDK 6的锁升级),性能已大幅提升。
1. synchronized的用法
- 修饰方法:锁是当前对象实例(非静态方法)或类对象(静态方法)。
- 修饰代码块:锁是
()
中的对象(可自定义锁对象)。
// 修饰非静态方法(锁为this)
public synchronized void method1() { ... }// 修饰静态方法(锁为类对象)
public static synchronized void method2() { ... }// 修饰代码块(锁为obj)
public void method3() {synchronized (obj) { ... }
}
2. 底层实现:从对象头到monitor
synchronized
的实现依赖对象头和monitor(监视器锁):
- 对象头:Java对象在内存中的布局包含对象头,其中
Mark Word
字段存储锁状态(如无锁、偏向锁、轻量级锁、重量级锁)。 - monitor:一种由C++实现的内核级锁(
ObjectMonitor
),包含等待队列、持有线程等信息。
JDK 6之前:synchronized
直接使用monitor,属于重量级锁,会导致线程从用户态切换到内核态,性能开销大。
3. JDK 6的锁升级优化
为提升性能,JDK 6引入了锁升级机制,让synchronized
从无锁状态逐步升级为重量级锁,避免一开始就使用昂贵的monitor:
- 无锁状态:对象刚创建,未被任何线程锁定。
- 偏向锁:若只有一个线程获取锁,会在对象头记录线程ID,后续该线程可直接获取锁(无需CAS),减少无竞争场景的开销。
- 轻量级锁:当有第二个线程竞争锁时,偏向锁升级为轻量级锁,通过CAS竞争锁(线程不会阻塞,而是自旋尝试)。
- 重量级锁:当竞争激烈(自旋失败),轻量级锁升级为重量级锁,依赖monitor实现,未获取锁的线程进入阻塞状态。
锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(不可逆,只能升级不能降级)。
五、ReentrantLock:灵活的显式锁
ReentrantLock
是JDK 1.5引入的显式锁,实现了Lock
接口,功能与synchronized
类似(均为可重入锁),但提供更高的灵活性。
1. ReentrantLock的核心特性
- 可重入性:同一线程可多次获取锁(与
synchronized
一致),state
计数递增,释放时递减至0才完全释放。 - 公平锁与非公平锁:
- 非公平锁(默认):线程获取锁时直接尝试CAS,失败再入队列,可能导致线程饥饿,但性能更高。
- 公平锁:线程按等待队列顺序获取锁,需通过
new ReentrantLock(true)
创建,性能较低但更公平。
- 可中断锁:支持
lockInterruptibly()
方法,允许线程在等待锁时响应中断(synchronized
不可中断)。 - 超时获取锁:支持
tryLock(long time, TimeUnit unit)
,超时未获取锁则返回false
,避免无限等待。 - 条件变量:通过
newCondition()
获取Condition
对象,实现多条件等待(synchronized
仅能通过对象本身的wait()
/notify()
)。
2. 基本用法
Lock lock = new ReentrantLock(); // 非公平锁
// Lock lock = new ReentrantLock(true); // 公平锁try {lock.lock(); // 获取锁// 临界区代码
} finally {lock.unlock(); // 释放锁(必须在finally中,避免死锁)
}
3. 底层实现:基于AQS
ReentrantLock
通过内部类Sync
(继承AQS)实现,Sync
有两个子类:
NonfairSync
:非公平锁实现,tryAcquire()
直接CAS竞争锁。FairSync
:公平锁实现,tryAcquire()
会先检查队列是否有前驱节点,有则排队。
六、synchronized与ReentrantLock的核心区别
特性 | synchronized | ReentrantLock |
---|---|---|
锁类型 | 隐式锁(JVM自动管理) | 显式锁(需手动lock()/unlock()) |
可重入性 | 支持 | 支持 |
公平性 | 非公平锁 | 可选择公平/非公平锁 |
可中断性 | 不可中断 | 可中断(lockInterruptibly()) |
超时获取 | 不支持 | 支持(tryLock(long)) |
条件变量 | 仅一个(通过wait()/notify()) | 多个(通过Condition) |
性能 | JDK 6+优化后与ReentrantLock接近 | 高并发下略优(非公平锁) |
底层实现 | 基于对象头+monitor,锁升级机制 | 基于AQS框架,CAS+等待队列 |
适用场景 | 简单同步场景,代码简洁 | 复杂同步场景(如超时、中断、多条件) |
七、面试高频问题解析
1. 为什么说synchronized是可重入锁?
可重入锁指同一线程可多次获取同一把锁。synchronized
的可重入性体现在:
- 线程获取锁后,
Mark Word
记录线程ID,再次进入同步代码时无需重新竞争,只需增加锁计数器。 - 例如,一个同步方法调用另一个同步方法(同一对象锁),不会产生死锁。
2. AQS的等待队列和条件队列有什么区别?
- 等待队列:AQS的核心队列,存储获取锁失败的线程,用于实现锁的竞争与唤醒。
- 条件队列:由
Condition
维护,存储调用await()
的线程,需通过signal()
唤醒后进入等待队列重新竞争锁。 - 一个AQS只有一个等待队列,但可以有多个条件队列。
3. CAS的ABA问题如何解决?
- 解决方案:给变量增加版本号,每次修改时版本号递增,CAS时同时检查值和版本号。
- Java中
AtomicStampedReference
类通过"值+时间戳(版本号)"解决ABA问题。
4. 锁升级机制中,轻量级锁为什么要自旋?
轻量级锁的自旋是为了避免线程阻塞(阻塞/唤醒的开销远大于自旋)。当线程竞争不激烈时,自旋几次可能就获取到锁,无需进入阻塞状态。但自旋次数有限(JVM默认10次),避免CPU空耗。
5. 什么时候用synchronized,什么时候用ReentrantLock?
- 优先用
synchronized
:简单场景(如单条件同步),代码简洁,JVM自动管理,不易出错。 - 用
ReentrantLock
:需要公平锁、超时控制、中断功能或多条件等待的场景(如生产者-消费者模型多条件)。
总结
Java锁机制是并发编程的核心,从底层的CAS操作到AQS框架,再到synchronized
和ReentrantLock
的具体实现,每一层都有其设计智慧:
- CAS是无锁编程的基础,适合低并发场景。
- AQS是锁的通用框架,通过状态管理和队列实现同步逻辑。
- synchronized是JVM内置锁,经过优化后性能优异,适合简单场景。
- ReentrantLock是灵活的显式锁,适合复杂同步需求。