CAS锁机制(无锁、自旋锁、乐观锁、轻量级锁)

本文详细介绍了CAS(compare and swap)机制的基本原理及其在Java中的应用。包括CAS如何解决多线程环境下的原子性问题,与synchronized关键字的区别,以及使用时需要注意的问题。
该文章已生成可运行项目,

什么是CAS机制(compare and swap)

CAS算法的作用:解决多线程条件下使用锁造成性能损耗问题的算法,保证了原子性,这个原子操作是由CPU来完成的
CAS的原理:CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值。
(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。
(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。
注意:
(1)预期原始值(A)是从偏移位置读取到三级缓存中让CPU处理的值,修改后的新值是预期原始值经CPU处理暂时存储在CPU的三级缓存中的值,而内存指定偏移位置中的原始值。
(2)比较从指定偏移位置读取到缓存的值与指定内存偏移位置的值是否相等,如果相等则修改指定内存偏移位置的值,这个操作是操作系统底层汇编的一个原子指令实现的,保证了原子性

  • JVM中CAS是通过UnSafe类来调用操作系统底层的CAS指令实现。
  • CAS基于乐观锁思想来设计的,其不会引发阻塞,synchronize会导致阻塞。

原子类

java.util.concurrent.atomic包下的原子类都使用了CAS算法。而java.util.concurrent中的大多数类的实现都直接或间接的使用了这些原子类。
Unsafe类使Java拥有了类似C语言指针操作内存空间的能力,同时也带来了指针的安全问题。

AtomicInteger原子类

AtomicInteger等原子类没有使用synchronized锁,而是通过volatile和CAS(Compare And Swap)解决资源的线程安全问题。
(1)volatile保证了可见性和有序性
(2)CAS保证了原子性,而且是无锁操作,提高了并发效率。

//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//成员变量value是在内存地址中距离当前对象首地址的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;

static {
    try {
        //类加载的时候,在静态代码块中获取变量value的偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
// 当前AtomicInteger原子类的value值
private volatile int value;

//类似于i++操作
public final int getAndIncrement() {
	//this代表当前AtomicInteger类型的对象,valueOffset表示value成员变量的偏移量
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
================================上方为AtomicInteger类中的方法,下方为Unsafe类中的方法=========================================================
//此方法的作用:获取内存地址为原子对象首地址+原子对象value属性地址偏移量, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
    int v;
    do {
    	//通过对象和偏移量获取变量值作为期望值,在修改该内存偏移位置的值时与原始进行比较
    	//此方法中采用volatile的底层原理,保证了内存可见性,所有线程都从内存中获取变量vlaue的值,所有线程看到的值一致。
        v= this.getIntVolatile(obj, offset);
    /*
	while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过obj和offset获取变量的值
	如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环
	如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环
	Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断
	*/
    } while(!this.compareAndSwapInt(obj, offset, v, v + delta));

    return v;
}

操作步骤:
(1)获取AtomicInteger对象首地址指定偏移量位置上的值,作为期望值。
(2)取出获取AtomicInteger对象偏移量上的值,判断与期望值是否相等,相等就修改AtomicInteger在内存偏移量上的值,不相等就返回false,重新执行第一步操作,重新获取内存指定偏移量位置的值。
(3) 如果相等,则修改值并返回true。
注意:从1、2步可以看CAS机制实现的锁是自旋锁,如果线程一直无法获取到锁,则一直自旋,不会阻塞

CAS和syncronized的比较

CAS线程不会阻塞,线程一致自旋
syncronized会阻塞线程,会进行线程的上下文切换,会由用户态切换到内核态,切换前需要保存用户态的上下文,而内核态恢复到用户态,又需要恢复保存的上下文,非常消耗资源。

CAS的缺点

(1)ABA问题
如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。
解决办法: 使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JUC包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

(2)循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销

(3)只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作
解决办法:
从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作

CAS使用注意事项

(1)CAS需要和volatile配合使用

CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性

(2)CAS适用于并发量不高、多核CPU的情况

CPU多核情况下可以同时执行,如果不合适就失败。而并发量过高,会导致自旋重试耗费大量的CPU资源

本文章已经生成可运行项目
<think> 首先,用户的问题是关于CAS和自旋加的原理、应用及区别。我需要根据提供的引用资料来回答。 引用内容总结: - 引用[1]: CAS是底层原子操作,自旋锁基于CAS实现。CAS失败时自旋重试。属于乐观并发控制,低竞争性能好。 - 引用[2]: CAS是硬件指令(如cmpxchg),从主内存读取数据。自旋锁通过while循环包裹CAS,直到成功。 - 引用[3]: 自旋锁容易造成busy-waiting。CAS原子语义常与自旋锁一起使用。 - 引用[4]: 自旋锁依赖原子操作(如CAS)实现,使用内存屏障确保可见性。流程图显示线程尝试获取、检查状态、自旋或执行临界区。 - 引用[5]: 自旋锁是多线程同步机制,保护共享资源,类似互斥和读写。 用户问题:CAS和自旋加的原理、应用及区别。注意,用户说“CAS”可能指的是使用CAS操作来实现,类似于自旋锁。 关键点: - CAS (Compare-And-Swap):是一个原子操作,比较内存位置的值与预期值,如果相等则修改为新值。 - 自旋锁 (Spinlock):一种锁机制,线程在尝试获取失败时,会循环(自旋)等待直到成功获取。通常使用CAS实现。 原理: - CAS原理:基于硬件指令(如x86的cmpxchg),确保操作的原子性。它涉及读取内存值、比较、交换,整个过程是原子的。 - 自旋锁原理:使用CAS操作来尝试获取。如果空闲(值为0),则CAS设置为占用(1);如果失败(被占用),线程在循环中不断尝试CAS操作,直到成功。 应用: - CAS应用:用于实现无数据结构、原子变量、自旋锁等。在并发编程中广泛使用。 - 自旋锁应用:适用于低竞争、短临界区保护。如操作系统内核、数据库系统、Redis等。 区别: - CAS是一个原子操作指令,而自旋锁是一种基于CAS的同步机制。 - CAS可以单独使用,例如实现原子计数器;自旋锁依赖于CAS来实现其获取逻辑。 - CAS操作本身不涉及等待;自旋锁在失败时会自旋(忙等待),直到成功。 根据引用[1]和[2],CAS自旋锁实现的基础;自旋锁使用CAS操作。两者在低竞争环境下性能好。 回答结构: 1. 解释CAS的原理和应用。 2. 解释自旋锁的原理和应用。 3. 讨论它们的区别。 4. 添加引用标识。 5. 最后生成相关问题。 系统指令: - 所有行内数学表达式用$...$,如$x^2$。 - 独立公式用$$...$$。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如[^1]。 用户可见层指令:回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 所以,我将组织回答: - 先介绍CAS。 - 然后介绍自旋锁。 - 比较区别。 - 应用场景。 - 相关问题。 确保回答真实可靠,基于提供的引用。 </think>### CAS和自旋加的原理、应用及区别 以下内容基于您提供的引用资料(包括引用[1]-[5]),我将逐步解释CAS(即使用CAS操作实现锁机制)和自旋加(即自旋锁)的原理、应用和核心区别。CAS(Compare-And-Swap)是一种底层原子操作指令,而自旋锁是一种基于CAS的同步机制,两者在并发编程中密切相关。 #### 1. **CAS的原理和应用** - **原理**: - CAS是一种硬件级别的原子操作指令(如x86架构的`cmpxchg`指令)。它比较内存位置的值与预期值:如果相等,则更新为新值;否则,不做任何操作。整个过程是原子的,确保在多线程环境下不会被中断[^2][^3]。 - 数学表示:CAS操作可用公式描述为: $$ \text{CAS}(V, E, N) = \begin{cases} \text{成功} & \text{if } V = E \\ \text{失败} & \text{otherwise} \end{cases} $$ 其中,$V$是内存地址的当前值,$E$是预期值,$N$是新值。 - CAS操作直接从主内存读取数据(而非缓存),确保可见性[^2]。它依赖于硬件指令(如CPU的LOCK指令)保证原子性,避免数据竞争[^4]。 - **应用**: - 实现无数据结构:如原子计数器、无队列等,CAS可直接用于更新共享变量,无需传统锁机制。 - 作为自旋锁的基础:自旋锁通过循环调用CAS来尝试获取(见下文)。 - 场景:适用于低竞争环境(如高并发但冲突少的场景),因为CAS操作轻量,避免了上下文切换开销[^1][^3]。 #### 2. **自旋加的原理和应用** - **原理**: - 自旋锁是一种乐观并发控制机制。线程尝试获取时,使用CAS操作检查状态(通常状态为0表示空闲,1表示占用)。如果CAS成功(空闲),线程获取并进入临界区;如果失败(被占用),线程在循环中“自旋”(忙等待),不断重试CAS操作,直到成功[^1][^2][^4]。 - 核心逻辑伪代码: ```python while not CAS(lock_address, 0, 1): # 尝试将从0(空闲)设置为1(占用) pass # 自旋等待 # 临界区代码 release_lock(lock_address) # 释放,将重置为0 ``` - 自旋锁依赖于内存屏障(如`mfence`指令)确保状态变化对所有线程可见,防止指令重排序[^4]。 - 流程图简化表示: ``` 线程尝试获取 -> 检查状态(使用CAS) -> 空闲? -> 是:获取并执行临界区 -> 否:自旋等待 -> 重试检查 ``` - **应用**: - 短临界区保护:适用于临界区执行时间短的场景(如计数器更新、简单状态变更),因为自旋等待避免了线程阻塞的开销[^1][^3]。 - 系统级编程:如操作系统内核、数据库系统(如Redis的自旋锁实现),在高性能场景下减少竞争延迟[^4][^5]。 - 低竞争环境:在CPU核心数多但线程冲突少的系统中,性能优于传统互斥(如pthread_mutex)[^1]。 #### 3. **核心区别** | 特性 | CAS | 自旋加自旋锁) | |------------------|----------------------------------|-----------------------------------| | **本质** | 底层原子操作指令(硬件支持) | 基于CAS的同步机制(软件实现) | | **等待行为** | 无等待:操作瞬间完成,失败即返回 | 忙等待(自旋):失败时循环重试 | | **实现复杂度** | 简单:单条指令操作 | 较高:需结合循环和CAS逻辑 | | **适用场景** | 原子变量更新、无数据结构 | 短临界区保护、低竞争并发控制 | | **性能影响** | 低开销,但高竞争时CAS失败率高 | 低竞争时高效;高竞争时CPU浪费(busy-waiting)[^3] | | **依赖关系** | 可独立使用 | 必须依赖CAS操作实现[^1][^2] | 总结区别: - CAS是一个原子操作,而自旋锁是一个基于CAS锁机制CAS是“工具”,自旋锁是“使用该工具的策略”[^1][^2]。 - CAS不涉及等待,自旋锁在失败时通过自旋实现乐观并发控制,两者都属于乐观锁范畴,在低竞争下性能优于悲观(如互斥)[^1][^5]。 #### 4. **典型应用场景对比** - **CAS场景**: - 实现原子变量(如Java的`AtomicInteger`),其中CAS直接用于更新值。 - 无算法(如并发队列),避免带来的死风险[^3]。 - **自旋加场景**: - Redis的自旋锁实现:保护共享资源(如缓存键),在短时操作中减少线程切换[^5]。 - 操作系统内核:自旋锁用于中断处理等短临界区,确保响应速度[^4]。 #### 注意事项 - **缺点**: - CAS:在高竞争下可能导致大量失败重试(需结合退避算法优化)。 - 自旋锁:长时间自旋浪费CPU资源(busy-waiting),不适用于长临界区或高竞争场景[^3][^5]。 - **最佳实践**:在Java或C++中,优先使用语言库提供的原子类(如`std::atomic`)或自旋锁实现(如`pthread_spinlock_t`),避免手动实现错误。 [^1]: CAS自旋锁实现的基础,自旋锁通常使用 CAS 操作来实现无的同步机制。 [^2]: CAS对应的是硬件指令cmpxchg从CPU做了原子性支持,执行中LOAD数据会访问主内存。 [^3]: 获取的线程一直处于活跃状态,但是并没有执行任何有效的指令,使用这种容易造成 busy-waiting。 [^4]: 自旋锁依赖于原子操作来实现线程安全的获取和释放,内存屏障则确保在多核环境下,的状态变化对所有线程可见。 [^5]: 自旋锁是一种多线程同步机制,用于保护共享资源,确保在同一时刻只有一个线程能够访问特定的资源。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值