轻量级锁的CAS操作实现原理
让我详细解释轻量级锁的CAS操作实现原理。我会从简单到复杂逐步展开。
1. CAS基本概念
首先,让我们理解什么是CAS(Compare And Swap)。想象你在改一份共享文档:
// CAS的基本思想
class Document {
private int version; // 文档版本号
private String content; // 文档内容
boolean updateContent(int expectedVersion, String newContent) {
// 检查版本号是否匹配,如果匹配才更新
if (version == expectedVersion) {
content = newContent;
version++;
return true;
}
return false;
}
}
这就像你在修改文档前先确认没人改过它,这个过程必须是原子的(不可分割的)。
2. 轻量级锁的实现步骤
让我们详细看看轻量级锁获取的完整过程:
public class LightweightLockExample {
private Integer value = 0; // 要同步的对象
public void synchronizedMethod() {
synchronized(value) { // 这里会触发轻量级锁
// 临界区代码
}
}
}
实现过程分为以下几步:
2.1 创建锁记录(Lock Record)
// 在线程栈中创建锁记录(简化的代码示意)
class LockRecord {
private Object owner; // 持有的对象
private long originalMarkWord; // 对象原始的Mark Word
public LockRecord(Object obj) {
this.owner = obj;
this.originalMarkWord = obj.markWord;
}
}
每个线程在执行同步代码时,会在自己的栈帧中创建一个锁记录空间。
2.2 CAS替换对象头
接下来是最关键的CAS操作:
class LightweightLocking {
// 用本地方法实现CAS操作
native boolean compareAndSwapMarkWord(Object obj,
long expectedValue,
long newValue);
boolean acquireLock(Object obj) {
// 创建锁记录
LockRecord lockRecord = createLockRecord(obj);
// 构造新的Mark Word
// 其中包含了指向Lock Record的指针
long newMarkWord = constructMarkWord(lockRecord);
// CAS替换对象头的Mark Word
return compareAndSwapMarkWord(obj,
lockRecord.originalMarkWord,
newMarkWord);
}
}
在底层,这个CAS操作会被转换为CPU的原子指令:
; x86架构下的CAS指令示意
; cmpxchg指令实现原子的比较和交换
mov rax, [expectedValue] ; 加载期望值
lock cmpxchg [address], rbx ; 原子地比较和交换
2.3 处理竞争情况
当CAS操作失败时,需要处理锁升级:
class LockUpgrade {
void handleLockContention(Object obj) {
// 先尝试自旋
for (int i = 0; i < MAX_SPIN; i++) {
if (tryAcquireLock(obj)) {
return;
}
// 自旋等待
Thread.onSpinWait(); // CPU提示,优化自旋
}
// 自旋失败,升级为重量级锁
inflateToHeavyweight(obj);
}
}
3. JVM中的具体实现
在HotSpot VM中,轻量级锁的实现位于synchronizer.cpp:
// HotSpot中的轻量级锁获取过程(简化版)
bool ObjectSynchronizer::tryAcquireLock(oop obj) {
// 获取对象的Mark Word
markOop mark = obj->mark();
// 如果已经是轻量级锁
if (mark->is_locked() && !mark->is_heavyweight()) {
// 检查是否是当前线程的锁记录
if (mark->locker() == thread->lock_record()) {
// 重入计数增加
thread->lock_record()->inc_recursions();
return true;
}
}
// 创建新的锁记录
BasicLockObject* lock = thread->lock_record();
lock->set_obj(obj);
lock->set_displaced_header(mark);
// 尝试CAS替换Mark Word
if (Atomic::cmpxchg(lock, obj->mark_addr(), mark) == mark) {
return true;
}
// CAS失败,处理竞争
return handleLockContention(obj, lock);
}
4. 性能考虑
轻量级锁的CAS实现考虑了多个性能因素:
class LockPerformance {
// 缓存行对齐
@sun.misc.Contended // 防止伪共享
private static class PaddedLock {
volatile long markWord;
// 填充以避免临近变量的缓存行冲突
long p1, p2, p3, p4, p5, p6, p7;
}
// 自旋优化
private static final int SPIN_LIMIT = 50;
// 重入优化
private static class LockRecord {
int recursionCount; // 重入计数
// ... 其他字段
}
}
5. 实际应用示例
让我们看一个完整的例子:
public class OptimisticLocking {
private volatile long value;
public void optimisticUpdate(long newValue) {
long current;
do {
// 读取当前值
current = value;
// 如果没人修改过,就更新成功
} while (!compareAndSet(current, newValue));
}
// 底层调用Unsafe的CAS操作
private native boolean compareAndSet(long expected, long newValue);
}
这种实现方式:
- 不需要重量级的监视器
- 适合竞争不激烈的场景
- 失败时可以快速重试
- 避免了线程调度的开销
通过这些机制,轻量级锁在大多数情况下都能提供比重量级锁更好的性能。您对哪部分还有疑问吗?欢迎评论区讨论。
面试小贴士
以下是几个关于轻量级锁和CAS的高频面试题,每个问题后面都附有简要答案:
-
什么是CAS?请解释其基本原理。
答:CAS是Compare And Swap的缩写,是一种无锁算法。它包含三个操作数——内存位置、预期原值及新值。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,否则不做任何操作。
-
CAS在Java中的应用有哪些?
答:在Java中,CAS主要有以下应用:
- Atomic包中的原子操作类,如AtomicInteger、AtomicBoolean等
- 并发容器如ConcurrentHashMap
- JVM中的轻量级锁实现
-
轻量级锁的获取过程是怎样的?为什么要用CAS?
答:轻量级锁的获取分为三步:
- 在当前线程的栈帧中创建锁记录(Lock Record)
- 将锁记录中的Lock Word复制到对象头的Mark Word中
- 用CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针
使用CAS是为了在多线程竞争的情况下,确保只有一个线程能成功获得锁。
-
如果CAS操作失败了会怎样?轻量级锁会如何处理?
答:如果CAS操作失败,说明有其他线程同时在尝试获取该锁。这时,当前线程会先自旋一段时间,尝试再次获取锁。如果自旋到一定次数还没获取到,轻量级锁会膨胀为重量级锁,进入重量级锁的流程。
-
什么是锁膨胀?什么情况下会发生?
答:锁膨胀是指轻量级锁升级为重量级锁的过程。当有多个线程同时竞争轻量级锁,导致自旋超过一定次数时,就会发生锁膨胀。膨胀后,线程将进入Monitor的EntryList队列,等待被唤醒。
-
轻量级锁的实现考虑了哪些性能因素?
答:主要有以下几个考虑:
- 空间局部性:Lock Record在线程栈上分配,访问速度快
- 缓存行填充:避免false sharing带来的性能下降
- 减少线程切换:CAS操作失败时先自旋,而不是直接阻塞
- 重入优化:每个Lock Record中都有一个recursion字段,记录重入次数
-
CAS操作有哪些潜在的问题?
答:CAS操作主要有三个问题:
- ABA问题:一个值从A变为B又变回A,CAS会误认为它没有变化
- 循环时间长开销大:如果CAS失败,会一直循环尝试,耗费CPU
- 只能保证一个共享变量的原子操作:对多个共享变量操作时,CAS无法保证原子性