J.U.C Review - CAS的工作原理
文章目录
- 悲观锁与乐观锁
- 悲观锁
- 乐观锁
- CAS(Compare And Swap)
- CAS的工作流程
- 示例
- Java中CAS的实现——Unsafe类
- `Unsafe`类中的CAS方法
- 通过`AtomicInteger`类的示例分析CAS操作
- CAS实现中的问题及解决方案
- ABA问题
- 自旋开销
- 单变量原子操作的局限性
悲观锁与乐观锁
在并发编程中,锁机制用于控制多个线程对共享资源的访问,防止数据的不一致性。根据锁的处理方式,可以将锁分为两种主要类型:悲观锁和乐观锁。
悲观锁
悲观锁是一种严格的资源访问控制方式,其假设在资源的每次访问时都会发生冲突,因此它总是在访问资源前将其锁定,以确保同一时刻只有一个线程能够访问该资源。这种策略有效防止了并发修改导致的数据不一致性问题,但也可能导致性能瓶颈,尤其在资源访问频繁的情况下。
使用悲观锁时,线程在进入临界区前会尝试获取锁,如果锁已被其他线程占用,则当前线程将进入等待状态,直到获得锁为止。常见的悲观锁实现包括数据库中的行级锁和表级锁、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的工作流程
- 读取当前变量值V:假设当前线程想要更新一个共享变量V。
- 比较V与E:线程将当前的V与预期值E进行比较。如果V等于E,说明没有其他线程修改该变量,操作可以继续。
- 执行更新操作:如果V与E相等,线程将V更新为新值N,操作成功。
- 处理操作失败:如果V与E不相等,说明有其他线程已经修改了V,此时操作失败,线程可以选择重试或放弃操作。
示例
假设我们有一个共享变量i
的初始值为5,线程A希望将其更新为6。线程A将通过以下步骤完成操作:
- 读取
i
的当前值V=5。 - 比较
i
的值是否等于预期值E=5。 - 如果
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
包中的一个类,用于实现整型的原子操作。
我们通过AtomicInteger
的getAndAdd(int delta)
方法来深入了解CAS的实现。
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
在上述代码中,U
是一个Unsafe
对象,VALUE
是AtomicInteger
类中定义的一个常量,表示该对象中目标字段的内存偏移量。
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
getAndAdd
方法调用了Unsafe
的getAndAddInt
方法,该方法的源码如下:
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;
}
解析
- 获取当前值:
getIntVolatile(o, offset)
读取对象o
中偏移量为offset
的字段值,并将其赋给v
。 - CAS操作:
weakCompareAndSetInt(o, offset, v, v + delta)
尝试使用CAS将v
的值更新为v + delta
。如果CAS操作成功,循环结束;否则,继续尝试。 - 返回旧值:方法返回的是操作前的旧值
v
。
weakCompareAndSet
的特殊性
weakCompareAndSetInt
是compareAndSetInt
的弱版本,它的执行更轻量级,但在一些情况下可能会失败,而不一定遵循严格的内存顺序。在JDK 9及更高版本中,weakCompareAndSetInt
带有@HotSpotIntrinsicCandidate
注解,这意味着它可能在JVM中被直接用汇编或IR(中间表示)代码实现,以提高性能。
CAS实现中的问题及解决方案
尽管CAS是实现无锁并发操作的有力工具,但它也存在一些固有的问题。
ABA问题
ABA问题指的是一个变量值从A变为B,又变回A,此时CAS检测不到值的变化,从而错误地认为没有其他线程修改该值。这在某些场景下可能导致逻辑错误。
解决方案:引入版本号或时间戳机制。Java的AtomicStampedReference
类提供了解决方案,该类在每次更新值时同时更新版本号,从而保证即使值相同,也能检测到版本号的变化。
自旋开销
由于CAS操作是一个循环重试机制,如果长时间无法成功,可能导致自旋消耗大量CPU资源,降低系统性能。
解决方案:在重试次数超过一定阈值后,线程可以选择放弃CAS操作,转而采用锁机制来保证安全性。
单变量原子操作的局限性
CAS操作只能保证一个变量的原子性,无法直接用于处理多个变量的复合操作。
解决方案:对于多个变量的操作,可以使用锁机制来保证原子性,或者采用AtomicReference
类包装对象,通过CAS操作来处理对象的原子性。