Java锁机制详解:从悲观锁、乐观锁到CAS自旋锁
本文详细阐述了Java中悲观锁、乐观锁的工作原理及CAS技术的底层实现。通过对比synchronized、ReentrantLock与AtomicReference的代码差异,解析了多线程环境下如何保障数据一致性,并探讨了自旋锁的性能特征与局限性。
核心概念:悲观锁与乐观锁
在多线程并发编程中,锁机制主要用于解决资源竞争问题。根据对冲突概率的假设不同,锁可分为两大类:
- 悲观锁(Pessimistic Lock):假定最坏情况,即认为每次获取数据时其他线程都会修改它。因此,在操作数据前必须先上锁,阻塞其他线程直到锁释放。Java中的
synchronized关键字和ReentrantLock类属于典型的悲观锁实现。 - 乐观锁(Optimistic Lock):假定最好情况,即认为其他线程不会修改数据,因此不加锁直接操作。但在更新时会检查数据是否被他人修改过(通常通过版本号或CAS算法),若发现冲突则重试或失败。
悲观锁实战:synchronized与ReentrantLock
当多个线程并发修改共享变量时,若不加控制会导致结果错误。以下演示如何通过悲观锁解决此问题。
第1步:无锁环境下的线程安全问题
首先观察未使用同步机制时的风险。50个线程各执行1000次自增,预期结果为50000,但实际运行往往小于该值:
public class LockDemo {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 50; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; ++j) {
count++;
}
});
thread.start();
threadList.add(thread);
}
for (Thread thread : threadList) {
thread.join();
}
System.out.println(count); // 结果通常小于50000
}
}
第2步:使用synchronized关键字加锁
将临界区代码包裹在synchronized块中,确保同一时刻只有一个线程能修改count:
// ... 前面代码相同
for (int i = 0; i < 50; i++) {
Thread thread = new Thread(() -> {
synchronized (LockDemo.class) {
for (int j = 0; j < 1000; ++j) {
count++;
}
}
});
// ... 后续代码相同
}
这种互斥机制保证了操作的原子性,最终输出稳定为50000。
第3步:使用ReentrantLock替代
ReentrantLock提供了更灵活的显式锁控制,需注意在finally块中解锁以防死锁:
Lock lock = new ReentrantLock();
// ... 在循环内
lock.lock();
try {
for (int j = 0; j < 1000; ++j) {
count++;
}
} finally {
lock.unlock();
}
两者均属于悲观锁,通过阻塞非持有线程来保证安全,但也带来了上下文切换的开销。
乐观锁实现:版本号机制
乐观锁避免了线程阻塞,常见于数据库场景。通过在表中增加version字段,利用SQL语句的原子性实现检查与更新:
select version from e_user where name = 'zs';
update e_user set password = 'admin', version = version + 1
where name = 'zs' and version = 1;
若两个线程A、B同时读取版本号为1,A先执行更新将版本改为2成功;B再执行时,因条件version = 1不匹配而失败,从而避免覆盖修改。
CAS算法与AtomicInteger原理
CAS(Compare-And-Swap)是乐观锁在内存层面的实现基础。Java标准库提供了AtomicInteger来处理整数的原子操作:
static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子自增
其底层依赖Unsafe类的compareAndSwapInt方法,伪代码如下:
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
流程解析: 1. 线程读取当前主存值var5。 2. 计算期望的新值(var5+1)。 3. CAS尝试将旧值var5替换为新值。若此时主存值仍为var5,则替换成功;否则说明中间有其它线程修改,循环重试(自旋)。
手写简易自旋锁
基于CAS原理,我们可以手动实现一个记录线程对象的自旋锁:
public class SpinLock {
private AtomicReference<Thread> holder = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!holder.compareAndSet(null, current)) {}
}
public void unlock() {
Thread current = Thread.currentThread();
holder.compareAndSet(current, null);
}
}
使用方式类似于传统锁,但需注意自旋锁的缺点:
1. CPU占用高:长时间争用锁会导致线程空转消耗CPU。
2. ABA问题:值在T1变为A,后又变回A,CAS无法识别过程变化。
3. 适用范围窄:仅适合轻量级原子操作。
总结
悲观锁通过阻塞等待保障安全,适用于写多读少场景;乐观锁通过重试机制减少阻塞,适用于读多写少且冲突低的场景。开发者应根据具体业务负载选择合适的锁策略。以上就是Java锁机制详解:从悲观锁、乐观锁到CAS自旋锁的详细内容,更多关于Java多线程编程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









