从 CAS 到 AQS:Java 并发编程的锁机制演进全解析,一文搞懂底层原理!


在并发编程领域,锁是保障共享资源在多线程环境下安全访问的核心机制。传统的解决方案是"加互斥锁",但简单粗暴的锁机制往往会带来性能瓶颈。为了在安全性和性能之间找到平衡,Java的并发框架经历了从底层硬件支持的CAS操作,到synchronized的锁升级策略,再到灵活强大的AQS框架的演进历程。

本文将带你从最基础的原子操作开始,一步步揭开Java并发编程的神秘面纱,让你真正理解这些"黑科技"是如何工作的。

CAS

什么是CAS?

CAS,即比较并替换(Compare-And-Swap),是现代处理器在硬件层面直接支持的一种原子指令(现代CPU普遍提供了CAS指令,如x86的CMPXCHG)。它为我们提供了一种独特的"无锁"编程方式——这里的"无锁"指的是不使用传统的互斥锁,但在硬件层面仍然通过锁定总线或缓存行来保证操作的原子性。

想象一下这样的场景:你要更新一个银行账户的余额,但你需要确保在你读取余额到更新余额的整个过程中,没有其他人同时修改这个值。CAS正是为解决这类问题而生的硬件级解决方案。

每个CAS操作都涉及三个核心参数,它们共同决定了操作的成败:

  • V (Memory Location):目标内存地址或变量的引用
  • E (Expected Value):我们期望该位置当前应该存储的值
  • N (New Value):我们希望设置的新值

CAS的执行遵循一个严格而简洁的逻辑:当且仅当内存地址V处的值等于预期值E时,才将该地址V处的值原子地更新为新值N。无论更新是否成功,都会返回V处在操作执行时的实际值。

这里的"原子地"是整个机制的核心——它意味着整个"比较并更新"的过程是不可中断的,要么完全成功,要么完全失败,绝不会出现中间状态。这种原子性是通过硬件层面的支持来保证的。

CAS体现了一种乐观锁的设计哲学:它假设在操作期间数据通常不会被其他线程修改,因此不预先加锁。相反,它采用"先尝试,后验证"的策略——先执行操作,如果在操作过程中发现数据已经被修改(即V不等于E),则操作失败,由调用方决定如何处理(通常是重试)。

这种设计思想与传统的悲观锁形成鲜明对比。传统互斥锁在获取锁失败时会导致线程阻塞和唤醒,而CAS在操作失败时不会使线程挂起,而是允许线程继续执行(例如,进行重试),从而显著减少了线程上下文切换的开销。

Java中的CAS实现:sun.misc.Unsafe

Java标准库中并没有直接暴露CAS操作给用户,但java.util.concurrent.atomic包下的原子类(如AtomicInteger, AtomicBoolean, AtomicReference等)广泛使用了CAS。它们底层依赖于一个特殊的类sun.misc.Unsafe,该类提供了如compareAndSwapInt(), compareAndSwapObject()等本地方法(C++实现),直接调用CPU的CAS指令。

// AtomicInteger.compareAndSet(expect, update) 的简化逻辑
public final boolean compareAndSet(int expect, int update) {
   
   
    // unsafe.compareAndSwapInt(this, valueOffset, expect, update)
    // this: 当前AtomicInteger对象
    // valueOffset: value字段在对象内存中的偏移量
    // expect: 期望值
    // update: 新值
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

CAS存在的问题

  1. ABA问题:ABA问题是CAS操作中最具迷惑性的问题之一。当一个值从A变为B,然后又变回A时,CAS操作会误认为值从未改变,从而执行错误的更新。这就像你离开房间时桌上放着一个苹果,回来时发现还是一个苹果,但实际上这期间苹果可能被别人拿走又放回了一个新的。

    这看起来是没什么问题, 但在链表、栈等数据结构操作中,ABA问题可能导致内存泄漏、野指针或数据结构破坏。比如在无锁栈的pop操作中,如果栈顶元素A被弹出后又被重新压入,CAS可能会误判并产生错误的指针操作。

    Java提供了AtomicStampedReference来解决ABA问题,其核心思想是为每个引用附加一个"版本戳"(stamp),类似于数据库中的版本号机制。AtomicStampedReference通过将引用和版本戳封装在一个不可变的Pair对象中,每次CAS操作都会同时比较引用值和版本戳,每次更新递增版本戳,版本戳永不重复,能捕获所有中间变化。其内部结构如下:

    private static class Pair<T> {
         
         
        final T reference;  // 实际的引用值
        final int stamp;    // 版本戳
    }
    
  2. 自旋开销: 如果CAS操作长时间不成功(即竞争激烈),会导致线程在循环中不断重试,空耗CPU资源。

  3. 只能保证单个共享变量的原子操作: 对于多个共享变量的原子性,CAS无能为力,需要更复杂的机制或将多个变量封装成一个对象再对该对象的引用进行CAS。

基于CAS的简单锁实现:自旋锁

有了CAS这个硬件级的原子操作基础,我们就可以在用户态构建一种简单而高效的锁机制——自旋锁。与传统的阻塞锁不同,自旋锁采用了一种"忙等待"的策略,通过不断轮询来获取锁资源。

自旋锁的工作原理

自旋锁的核心思想可以用一个生动的比喻来理解:想象你要使用一个公共电话亭,如果发现有人在使用,你不会离开去做其他事情,而是在旁边等待,时不时看一眼电话亭是否空闲。这就是自旋锁的基本工作模式。

线程1 线程2 自旋锁 初始状态: lockState=0 (未持有) lock() - 尝试获取电话亭 CAS(lockState, 0, 1) 获取成功,lockState=1 使用电话亭 lock() - 尝试获取电话亭 CAS(lockState, 0, 1) 失败 CAS(lockState, 0, 1) 失败,Thread.yield() loop [自旋] 忙等待中... unlock() - 释放电话亭 lockState=0 电话亭空闲 CAS(lockState, 0, 1) 获取成功,lockState=1 使用电话亭 线程1 线程2 自旋锁

当一个线程尝试获取自旋锁时,整个过程如下:

  1. 状态检查:线程首先检查锁的状态(通常是一个volatile变量,0表示未持有,1表示已持有)
  2. CAS尝试获取:如果锁未被持有,线程尝试通过CAS原子地将锁状态从0更新为1
  3. 获取成功:如果CAS操作成功,线程获得锁,可以进入临界区
  4. 自旋等待:如果锁已被其他线程持有或CAS失败,该线程进入"忙等待"循环,反复尝试获取锁

释放锁的过程相对简单:持有锁的线程直接将锁状态重置为0即可。

import java.util.concurrent.atomic.AtomicInteger;

public class SimpleSpinLock {
   
   
    private final AtomicInteger lockState = new AtomicInteger(0);
    private volatile Thread owner;
    
    public void lock() {
   
   
        Thread currentThread = Thread.currentThread();
        
        // 自旋等待,直到成功获取锁
        while (!lockState.compareAndSet(0, 1)) {
   
   
            // 这里是自旋的核心:不断重试
            // 可以添加短暂的让步,避免过度消耗CPU
            Thread.yield();
        }
        
        // 记录锁的持有者
        owner = currentThread;
    }
    
    public void unlock() {
   
   
        Thread currentThread = Thread.currentThread();
        
        // 只有持有锁的线程才能释放
        if (owner != currentThread) {
   
   
            throw new IllegalMonitorStateException("当前线程未持有锁");
        }
        
        owner = null;
        lockState.set(0); // 释放锁
    }
    
    public boolean isLocked() {
   
   
        return lockState.get() == 1;
    }
    
    public Thread getOwner() {
   
   
        return owner;
    }
}

自适应自旋

为了缓解自旋锁的CPU消耗问题,JVM引入了自适应自旋。这意味着自旋的次数不再是固定的,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。

  • 如果对于某个锁,自旋刚刚成功获得过,并且持有锁的线程正在运行,那么JVM会认为这次自旋也很可能再次成功,进而允许自旋持续更长时间。
  • 反之,如果对于某个锁,自旋很少成功,或者持有锁的线程长时间未释放,那么JVM可能会省略自旋过程,或减少自旋次数,以避免浪费CPU。
public class AdaptiveSpinLock {
   
   
    private final AtomicInteger lockState = new AtomicInteger(0);
    private volatile int spinCount = 100; // 动态调整的自旋次数
    private volatile long lastSuccessTime = System.nanoTime();
    
    public void lock() {
   
   
        Thread currentThread = Thread.currentThread();
        int attempts = 0;
        long startTime = System.nanoTime();
        
        while (!lockState.compareAndSet(0, 1)) {
   
   
            attempts++;
            
            // 根据历史表现调整自旋策略
            if (attempts > spinCount) {
   
   
                // 自旋次数过多,主动让出CPU
                Thread.yield();
                
                // 根据等待时间调整下次自旋阈值
                long waitTime = System.nanoTime() - startTime;
                if (waitTime > 1_000_000) {
   
    // 超过1ms
                    spinCount = Math.max(10, spinCount / 2);
                }
                break;
            }
        }
        
        // 成功获取锁,更新统计信息
        if (lockState.get() == 1) {
   
   
            long successInterval = System.nanoTime() - lastSuccessTime;
            lastSuccessTime = System.nanoTime();
            
            // 如果最近成功获取锁,适当增加自旋次数
            if (successInterval < 10_000_000) {
   
    // 10ms内成功过
                spinCount = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HeZephyr

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值