J.U.C Review - CAS的工作原理

在这里插入图片描述

悲观锁与乐观锁

在并发编程中,锁机制用于控制多个线程对共享资源的访问,防止数据的不一致性。根据锁的处理方式,可以将锁分为两种主要类型:悲观锁和乐观锁。

悲观锁

悲观锁是一种严格的资源访问控制方式,其假设在资源的每次访问时都会发生冲突,因此它总是在访问资源前将其锁定,以确保同一时刻只有一个线程能够访问该资源。这种策略有效防止了并发修改导致的数据不一致性问题,但也可能导致性能瓶颈,尤其在资源访问频繁的情况下。

使用悲观锁时,线程在进入临界区前会尝试获取锁,如果锁已被其他线程占用,则当前线程将进入等待状态,直到获得锁为止。常见的悲观锁实现包括数据库中的行级锁和表级锁、Java中的ReentrantLock等。

适用场景:悲观锁适用于“写多读少”的场景,尤其是在高并发环境下,频繁的写操作容易引发冲突,此时使用悲观锁可以有效避免冲突,保障数据的一致性。


乐观锁

乐观锁与悲观锁相反,它假设资源的访问通常不会发生冲突,因此不需要在每次访问时都加锁。乐观锁允许多个线程同时访问资源,并在操作结束时通过一种验证机制来检查是否发生了冲突,如果检测到冲突,则放弃当前操作并重新尝试。

乐观锁常通过CAS(Compare And Swap,比较并交换)机制来实现,CAS是一种硬件支持的原子操作,保证了即使在多线程环境下,也能安全地执行检查并更新操作。

适用场景:乐观锁适用于“读多写少”的场景,这种情况下,资源访问的冲突概率较低,使用乐观锁可以减少锁带来的开销,提高系统性能。


CAS(Compare And Swap)

CAS,全称为“比较并交换”,是一种用于实现乐观锁的底层机制。在CAS操作中,涉及三个关键的值:

  • V:当前操作的变量。
  • E:预期值(Expected),即操作前希望变量V处于的值。
  • N:新值(New),即希望将变量V更新为的值。

CAS的核心原理是:首先检查变量V是否等于预期值E,如果相等,则将V的值更新为新值N;如果不等,则表示已经有其他线程修改了V的值,当前操作失败,系统将放弃更新。这种机制确保了并发操作的安全性,并且不需要使用传统的锁。


CAS的工作流程

  1. 读取当前变量值V:假设当前线程想要更新一个共享变量V。
  2. 比较V与E:线程将当前的V与预期值E进行比较。如果V等于E,说明没有其他线程修改该变量,操作可以继续。
  3. 执行更新操作:如果V与E相等,线程将V更新为新值N,操作成功。
  4. 处理操作失败:如果V与E不相等,说明有其他线程已经修改了V,此时操作失败,线程可以选择重试或放弃操作。

示例

假设我们有一个共享变量i的初始值为5,线程A希望将其更新为6。线程A将通过以下步骤完成操作:

  1. 读取i的当前值V=5。
  2. 比较i的值是否等于预期值E=5。
  3. 如果i的值确实为5,线程A将其更新为6;如果i的值已经被其他线程修改为其他值(例如2),线程A的操作将失败,i的值保持不变。

因为CAS是一个原子操作,所以在执行步骤2和步骤3时,操作不会被中断,从而保证了操作的线程安全性。

Java中CAS的实现——Unsafe类

Java中通过Unsafe类来实现CAS操作。Unsafe类位于sun.misc包下,提供了一系列的native方法,这些方法由JVM在底层通过C或C++实现,以保证高效的原子操作。

Unsafe类中的CAS方法

Unsafe类中有几个与CAS操作相关的方法:

public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

这些方法接收的参数包括:

  • Object o:要操作的对象。
  • long offset:要操作的对象字段的内存偏移量。
  • expected:预期值。
  • x:新值。

这些compareAndSwap方法都是native方法,也就是说,它们的具体实现是在JVM的底层代码中,通常是通过硬件指令(如cmpxchg)来保证操作的原子性。


通过AtomicInteger类的示例分析CAS操作

在Java中,AtomicInteger类是java.util.concurrent.atomic包中的一个类,用于实现整型的原子操作。

在这里插入图片描述

我们通过AtomicIntegergetAndAdd(int delta)方法来深入了解CAS的实现。

public final int getAndAdd(int delta) {
    return U.getAndAddInt(this, VALUE, delta);
}

在上述代码中,U是一个Unsafe对象,VALUEAtomicInteger类中定义的一个常量,表示该对象中目标字段的内存偏移量。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

getAndAdd方法调用了UnsafegetAndAddInt方法,该方法的源码如下:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

解析

  1. 获取当前值getIntVolatile(o, offset)读取对象o中偏移量为offset的字段值,并将其赋给v
  2. CAS操作weakCompareAndSetInt(o, offset, v, v + delta)尝试使用CAS将v的值更新为v + delta。如果CAS操作成功,循环结束;否则,继续尝试。
  3. 返回旧值:方法返回的是操作前的旧值v

weakCompareAndSet的特殊性

weakCompareAndSetIntcompareAndSetInt的弱版本,它的执行更轻量级,但在一些情况下可能会失败,而不一定遵循严格的内存顺序。在JDK 9及更高版本中,weakCompareAndSetInt带有@HotSpotIntrinsicCandidate注解,这意味着它可能在JVM中被直接用汇编或IR(中间表示)代码实现,以提高性能。


CAS实现中的问题及解决方案

尽管CAS是实现无锁并发操作的有力工具,但它也存在一些固有的问题。

ABA问题

ABA问题指的是一个变量值从A变为B,又变回A,此时CAS检测不到值的变化,从而错误地认为没有其他线程修改该值。这在某些场景下可能导致逻辑错误。

解决方案:引入版本号或时间戳机制。Java的AtomicStampedReference类提供了解决方案,该类在每次更新值时同时更新版本号,从而保证即使值相同,也能检测到版本号的变化。


自旋开销

由于CAS操作是一个循环重试机制,如果长时间无法成功,可能导致自旋消耗大量CPU资源,降低系统性能。

解决方案:在重试次数超过一定阈值后,线程可以选择放弃CAS操作,转而采用锁机制来保证安全性。


单变量原子操作的局限性

CAS操作只能保证一个变量的原子性,无法直接用于处理多个变量的复合操作。

解决方案:对于多个变量的操作,可以使用锁机制来保证原子性,或者采用AtomicReference类包装对象,通过CAS操作来处理对象的原子性。

在这里插入图片描述

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小小工匠

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

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

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

打赏作者

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

抵扣说明:

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

余额充值