深入浅出Java多线程(十):CAS
引言大家好,我是你们的老店员秀才!今天带来的是[深入浅出Java多线程]系列的第十篇内容:CAS。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!
在多线程编程中,对共享资源的安全访问和同步控制是至关紧张的。传统的锁机制,如synchronized关键字和ReentrantLock等,能够有用防止多个线程同时修改同一数据导致的竞态条件(race condition),但同时也带来了一定的性能开销。尤其是在高并发场景下,频繁的加锁解锁操纵可能导致线程上下文切换加剧、体系响应耽误等题目。
为了应对这一挑战,Java从JDK 1.5版本开始引入了基于CAS(Compare And Swap)机制的原子类库,这些原子类不仅提供了一种无锁化的并发控制策略,还能够在不阻塞其他线程的情况下实现高效的内存同步。CAS作为乐观锁的一种实现方式,其焦点头脑是在更新变量时仅当该变量的当前值与预期值相称时才会实行更新操纵,否则就放弃更新并答应线程继续尝试或采取其他策略。
例如,在一个简单的场景中,假设有一个被多个线程共享的整型变量i,若我们想要通过CAS将其从初始值5原子性地递增到6,可以利用AtomicInteger类中的compareAndSet方法:
import java.util.concurrent.atomic.AtomicInteger;<br><br>public class CASExample {<br> private static AtomicInteger sharedValue = new AtomicInteger(5);<br><br> public static void main(String[] args) throws InterruptedException {<br> Thread t1 = new Thread(() -> {<br> while (true) {<br> int oldValue = sharedValue.get();<br> if (sharedValue.compareAndSet(oldValue, oldValue + 1)) {<br> System.out.println("Thread " + Thread.currentThread().getName() + " updated the value to " + (oldValue + 1));<br> break;<br> }<br> }<br> });<br><br> t1.start();<br> // 确保t1有机会更新值<br> t1.join();<br><br> // 输出结果应为:Thread Thread-0 updated the value to 6<br> }<br>}<br><br>在这个示例中,假如sharedValue的当前值确实是5,那么线程t1将成功地将它更改为6,并退出循环;假如有其他线程在此期间改变了sharedValue的值,则t1会不断重试直至成功。由于CAS操纵直接由CPU指令级别包管其原子性,因此不会出现因并发写入导致的数据混乱。
通过深入探究Java多线程中的CAS技能,我们将揭示其背后的具体实现原理——Unsafe类及其native方法,剖析AtomicInteger等原子类如何借助CAS机制实现在无锁情况下的高效并发操纵,并进一步讨论在实际应用中可能出现的题目,如ABA题目、循环自旋消耗过大以及只能针对单个变量进行原子操纵的局限性及其相应的解决方案。
在多线程编程领域中,锁机制是实现数据同步和避免并发题目的关键本领。其中,乐观锁与悲观锁作为两种差别的并发控制策略,在处理共享资源时接纳了截然差别的假设和操纵方式。
悲观锁&乐观锁
悲观锁
悲观锁,顾名思义,采取守旧的策略对待并发访问。它假定每次对共享资源进行操纵时都可能发生冲突,因此在实行任何更新前都会预先锁定资源。例如,在Java中使用synchronized关键字或ReentrantLock等工具实现悲观锁时,一个线程在获取锁后才气进入临界区实行代码,其他线程则必须等待锁开释后才气得到实行机会。以下是一个简单的悲观锁示例:
public class PessimisticLockExample {<br> private final Lock lock = new ReentrantLock();<br><br> public void decrementCounter() {<br> lock.lock(); // 获取悲观锁<br> try {<br> // 临界区代码<br> int count = this.count;<br> if (count > 0) {<br> this.count--;<br> }<br> } finally {<br> lock.unlock(); // 释放悲观锁<br> }<br> }<br><br> // 共享资源变量<br> private int count = 10;<br>}<br><br>在这个例子中,当一个线程试图修改计数器时,会先锁定整个方法,确保同一时间只有一个线程能够实行减一操纵。这种机制虽然包管了数据一致性,但可能造成线程间的频繁阻塞和上下文切换,尤其在高并发情况下性能损耗明显。
乐观锁
相对而言,乐观锁则是基于积极乐观的假设:以为大部分情况下多个线程同时访问同一资源并不会发生冲突。因此,乐观锁答应线程无须获取锁就可以实行业务逻辑,仅在更新数据时接纳CAS(Compare And Swap)原子操纵检查并更新数据。假如发现数据已被其它线程改变,则放弃本次更新,通常会重新读取数据并再次尝试。
以Java中的AtomicInteger为例,它利用CAS机制实现了乐观锁的特性:
public class OptimisticLockExample {<br> private final AtomicInteger counter = new AtomicInteger(10);<br><br> public void incrementCounter() {<br> while (true) { // 自旋<br> int currentValue = counter.get();<br> int newValue = currentValue + 1;<br> if (counter.compareAndSet(currentValue, newValue)) { // 使用CAS原子操作<br> break; // 更新成功,退出循环<br> }<br> }<br> }<br>}<br><br>// AtomicInteger 的 compareAndSet 方法源码简化示意<br>public final boolean compareAndSet(int expect, int update) {<br> return unsafe.compareAndSwapInt(this, valueOffset, expect, update);<br>}<br><br>上述代码展示了如何在一个循环内连续尝试原子地增加计数器值。只有当当前值等于预期值时,CAS操纵才会成功,否则线程将不断重试直至成功更新。由于乐观锁在没有冲突的情况下不涉及线程挂起,故实用于“读多写少”的场景,能有用低落加锁开销,提高体系吞吐量。然而,若并发更新频率较高,可能会导致大量的CAS失败和重试,从而带来额外的CPU消耗。
CAS原理
在并发编程中,CAS(Compare and Swap,比较并互换)是一种无锁算法,它在不阻塞其他线程的情况下实现原子性的变量更新操纵。在Java中,CAS的实现基于Unsafe类提供的native方法,这些方法直接与底层硬件交互,利用CPU级别的原子指令来包管数据更新的安全性。
CAS流程
在CAS操纵中涉及三个关键值:V(要更新的变量),E(预期值),N(新值)。当需要对一个共享变量进行修改时,线程起首检查该变量当前值是否等于预期值E。假如相称,则将变量值更新为新值N;假如不等,则阐明已经有其他线程更新了该变量,此时当前线程放弃更新操纵,保持原值稳定。
以AtomicInteger为例,我们可以通过以下代码片断明白CAS的工作过程:
import java.util.concurrent.atomic.AtomicInteger;<br><br>public class CASTest {<br> private AtomicInteger counter = new AtomicInteger(5);<br><br> public void increment() {<br> int expectedValue = counter.get();<br> while (!counter.compareAndSet(expectedValue, expectedValue + 1)) {<br> // 当前线程获取到的值已经被其他线程改变,重新获取最新值<br> expectedValue = counter.get();<br> }<br> }<br>}<br><br>在这个例子中,compareAndSet方法会不断尝试将计数器从旧值递增1,直到成功为止。当多个线程同时尝试增加计数器时,只有一个线程能够通过CAS成功更新,其余线程将继续循环直至其看到的预期值和实际值匹配后再尝试更新。
原子性和操纵体系
CAS的焦点优势在于其原子性——即整个比较和互换的操纵作为一个不可分割的整体实行。在现代多核CPU架构下,诸如cmpxchg指令这样的原子指令能够确保在没有外部干预的情况下完成这一系列步骤。在Linux X86体系中,cmpxchgl指令配合lock前缀可以确保在同一时候仅有一个处理器能成功更新内存位置,从而避免了并发题目。
ABA题目
尽管CAS机制在大多数情况下表现优异,但存在一种特殊情况——ABA题目。假设一个变量初始值为A,被更改为B后又改回A,这种情况下使用单纯的CAS检查将会误判为未发生过变革。为了应对ABA题目,JDK提供了一个名为AtomicStampedReference的类,它在每个对象引用上附加了一个版本号或时间戳,使得每次更改不仅检查引用自己,还检查版本号,只有两者都匹配时才会进行替换。
import java.util.concurrent.atomic.AtomicStampedReference;<br><br>public class ABATest {<br> private AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);<br><br> public void update(int newValue, int newStamp) {<br> while (true) {<br> int currentStamp = ref.getStamp();<br> if (ref.compareAndSet(1, newValue, currentStamp, newStamp)) {<br> break; // 更新成功<br> } else {<br> // 失败则重试,获取最新的stamp<br> }<br> }<br> }<br>}<br><br>在上述代码中,compareAndSet方法不仅要比较引用对象的值,还要比较并更新相干联的版本信息,因此有用防止了ABA题目的发生。
综上所述,CAS作为一种高效的无锁同步机制,在Java多线程编程中扮演着紧张角色,通过直接调用CPU指令实现了并发情况下的原子操纵,但也需要留意潜伏的ABA题目以及长时间自旋带来的性能开销等题目,并选择合适的解决方案。
Unsafe类
在Java中,为了能够直接与底层硬件进行交互并实行原子操纵,如CAS,Java使用了一个名为sun.misc.Unsafe的类。由于该类提供了一些不受JVM访问控制约束的方法,并答应开发者直接操纵内存和实行非安全但高效的原语操纵,因此被称为“Unsafe”。尽管这个类不在公共API中,但在并发包java.util.concurrent.atomic中的原子类,如AtomicInteger等,都依靠于Unsafe类提供的CAS操纵来包管线程间的原子性和可见性。
Unsafe类与CAS方法Unsafe类包含了一系列native方法,这些方法用于实行原子性的CAS操纵,例如:
public native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);<br>public native boolean compareAndSwapInt(Object o, long offset, int expected, int x);<br>public native boolean compareAndSwapLong(Object o, long offset, long expected, long x);<br><br>这些方法分别用于比较并互换对象引用、整型值以及长整型值。参数寄义如下:
[*]o:一个对象实例,CAS操纵将作用在其内部的一个字段上。
[*]offset:指定字段相对于对象起始地址的偏移量,由objectFieldOffset()方法计算得出。
[*]expected:盼望的旧值,只有当字段当前值等于此预期值时,才会进行更新。
[*]x:新值,假如条件满足,则用新值替换旧值。
以AtomicInteger为例,其getAndAddInt方法就利用了Unsafe类的compareAndSwapInt方法实现原子递增:
public final int getAndAddInt(Object o, long offset, int delta) {<br> int v;<br> do {<br> v = getIntVolatile(o, offset); // 获取当前值<br> } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS尝试更新<br> return v; // 返回更新前的值<br>}<br><br>这里起首获取到共享变量的当前值v,然后在一个循环中不断尝试通过CAS指令将变量从v更新为v+delta,直到成功为止。
CPU级别的原子操纵值得留意的是,CAS操纵在Java中的实现实际上调用了操纵体系和CPU提供的原子指令。在Linux X86体系下,是通过cmpxchgl这样的CPU指令实现的,而在多处理器情况中,为了确保跨多个CPU焦点的原子性,还需要配合lock前缀指令锁定总线或缓存行,防止其他处理器同时修改同一数据。
弱版本CAS与强版本CAS的区别从JDK 9开始,Unsafe类提供了两个看似相似但实际上可能有差别实现策略的方法:compareAndSetInt和weakCompareAndSetInt。虽然在早期版本中它们的行为一致,但在某些情况下,weakCompareAndSet系列方法可能只保留了volatile变量自己的特性,而放弃了happens-before规则带来的内存语义保障。这意味着weakCompareAndSet无法确保除了目标volatile变量以外的其他变量的操纵次序和可见性,从而有可能带来更高的性能,但也可能需要开发人员更小心地处理并发逻辑。
总之,Java通过Unsafe类实现了对CAS原子操纵的支持,使得步伐员可以在高级语言层面上利用底层硬件的原子指令,构建出高效且无锁化的并发步伐。然而,这也要求开发者具备对并发编程机制深刻的明白,以便正确解决潜伏的题目,比如ABA题目,以及合理应对CAS自旋可能导致的性能开销。
AtomicInteger源码简析
Java并发包中的java.util.concurrent.atomic.AtomicInteger类是一个基于CAS实现的线程安全整数容器,它提供了一系列原子操纵方法,如get、set、incrementAndGet等。以getAndAdd(int delta)方法为例,该方法用于获取当前值并原子性地将值增加指定的delta。
Java 17下的Atomic类:
https://img2024.cnblogs.com/blog/3378408/202403/3378408-20240311102336864-2005225704.png
起首,我们观察到getAndAdd(int delta)方法调用了Unsafe类的getAndAddInt()方法:
public final int getAndAdd(int delta) {<br> return U.getAndAddInt(this, VALUE, delta);<br>}<br><br>这里的U是Unsafe类的一个实例,其内部字段VALUE存储了AtomicInteger类中value变量相对于对象起始地址的偏移量。objectFieldOffset()方法用于计算这个偏移量:
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");<br><br>然后,深入到Unsafe类的getAndAddInt()方法实现:
public final int getAndAddInt(Object o, long offset, int delta) {<br> int v;<br> do {<br> v = getIntVolatile(o, offset); // 获取volatile类型的旧值<br> } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS更新新值<br> return v; // 返回更新前的值<br>}<br><br>这段代码展示了典范的CAS循环模式。起首通过getIntVolatile()读取内存中AtomicInteger实例的volatile变量value的当前值,并保存在局部变量v中。接下来进入一个do-while循环,在循环体内尝试使用weakCompareAndSetInt()实行CAS操纵。只有当value的当前值等于我们刚读取到的v时,才会将value设置为v+delta。假如此时value已经被其他线程更改,则CAS失败,步伐会再次读取新的value值,并重新进行CAS尝试,直到成功为止。
值得留意的是,这里虽然使用了weakCompareAndSetInt()方法,但在JDK 8及之前版本中,compareAndSetInt()和weakCompareAndSetInt()的功能实际上是雷同的。而在JDK 9及以上版本中,weakCompareAndSetInt()可能具有更弱的内存语义包管,即不逼迫满足happens-before规则,这有助于提升性能但要求开发者对并发编程有更深的明白。
通过这种方式,AtomicInteger借助Unsafe提供的底层支持实现了无锁的原子操纵,不仅避免了传统锁机制带来的上下文切换开销,还确保了在多线程情况下的数据一致性。同时,通过对源码的分析,我们可以更加深入地明白Java如何利用CAS机制来解决并发题目。
常见题目与解决方案
循环自旋开销题目及其解决方案
使用CAS通常伴随着循环重试机制,即当CAS失败时,线程会不断尝试再次实行CAS操纵直至成功。然而,在高竞争条件下,这可能导致线程长时间处于“自旋”状态,占用大量CPU资源且无实质性工作进展。
为相识决这一题目,JVM支持处理器提供的pause指令,比如在HotSpot虚拟机中,可以插入适当的pause指令来低落自旋等待过程中的CPU消耗。pause指令可以使CPU临时放弃当前线程的实行,并让其他线程有机会运行,从而减少空转带来的性能损失。此外,现代JVM还通过自适应自旋策略调整自旋次数,以达到更好的性能效果。
单变量原子操纵局限及其扩展方案
虽然CAS能很好地包管单个共享变量的原子性,但在涉及多个变量的操纵场景下,单纯的CAS将显得力有未逮。为了应对这种情况,有以下两种解决方案:
[*]使用AtomicReference类封装对象当需要对包含多个变量的对象进行原子性更新时,可以利用java.util.concurrent.atomic.AtomicReference类。将多个变量封装到一个对象中,然后对整个对象进行CAS操纵,如:
class Data {<br> int a;<br> int b;<br>}<br>AtomicReference<Data> atomicData = new AtomicReference<>(new Data(1, 2));<br>// 更新a和b字段的原子操作<br>Data newData = new Data(3, 4);<br>atomicData.compareAndSet(currentData, newData);<br><br>
[*]使用锁保护临界区在一些复杂的多变量操纵场景下,CAS可能无法直接满足需求,此时可以选择传统的锁机制,如synchronized关键字或ReentrantLock类来保护临界区代码,确保在给定时间内只有一个线程能够访问并更新这些变量,从而实现多变量操纵的原子性。
综上所述,虽然CAS带来了高效的无锁并发控制机制,但也存在诸如ABA题目、循环自旋开销过大以及只能处理单个变量等题目。针对这些题目,Java平台提供了相应的解决方案,如AtomicStampedReference类、pause指令优化以及AtomicReference等工具,资助开发者在复杂多样的并发场景下更机动地运用CAS技能。
总结
在Java多线程编程中,CAS(Compare and Swap)机制扮演着至关紧张的角色。作为乐观锁的一种实现方式,它通过比较并互换内存位置的值来包管原子操纵,避免了传统悲观锁带来的并发性能瓶颈和上下文切换开销。在JDK的java.util.concurrent.atomic包中,诸如AtomicInteger、AtomicStampedReference等原子类库就是基于Unsafe类提供的CAS原语构建的。
以AtomicInteger为例,其getAndAdd方法利用CAS循环实现了无锁的原子递增操纵,确保在高并发场景下变量更新的正确性和高效性。然而,CAS并非完善无缺,其中的ABA题目需要通过引入版本号或时间戳的方式来解决,如AtomicStampedReference通过比较引用与版本戳防止了两次雷同值之间的中间状态被忽视。
针对循环自旋导致的CPU资源浪费题目,现代JVM如HotSpot支持处理器pause指令,能够在自旋失败时低落CPU活动频率,减少不必要的消耗。同时,为了克服单个共享变量原子操纵的局限性,Java提供了AtomicReference类,可以封装多个变量作为一个整体进行CAS操纵,大概在必要时接纳锁机制,如synchronized关键字或ReentrantLock,确保多变量间的原子性。
综上所述,CAS为Java开发者提供了一种强大的无锁并发工具,但其使用需联合具体应用场景和可能碰到的题目机动选择解决方案。只有充分明白并合理应用CAS及其相干技能,才气在实际开发中编写出高性能且线程安全的多线程代码。尽管文档中未给出具体的代码实例,但上述分析和解释已经清晰描绘了如何在Java中运用CAS实现原子操纵以及应对相干挑战的过程。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]