Java锁机制详解:从悲观锁、乐观锁到CAS自旋锁

Ada
Ada
发布时间:2026-10-16 | 7人浏览过
原创

本文详细阐述了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多线程编程的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表