目录
一、什么是CAS
CAS全称 Compare And Swap(比较与交换),是一种无锁算法,是线程并发运行时用到的一种技术。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
二、CAS的运行原理
2.1 运行过程
CAS算法涉及到三个操作数:
- 需要读写的内存值 V;
- 进行比较的值 A;
- 要写入的新值 B;
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
2.2 自旋锁
自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
说到这里,不得不解析下自旋锁的概念:
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
对于互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
三、CAS底层实现分析
3.1 代码例子
使用atomic下的原子类AtomicInteger:
package com.ningzhaosheng.thread.concurrency.features.atom.cas;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author ningzhaosheng
* @date 2024/2/5 18:37:23
* @description 测试CAS
*/
public class TestCas {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
3.2 测试结果
从结果中我们可以看出,执行结果符合预期值,从而得出:使用CAS基础上提供的原子类AtomicInteger,从而解决并发安全问题。
3.3 分析代码
3.3.1 java 层面
我们可以分析下AtomicInteger源码,从上图中可以看到,AtomicInteger在初始化时,会初始化一个Unsafe类,然后通过这个Unsafe类调用objectFieldOffset方法获取到AtomicInteger的初始值。其实就是我们在new AtomicInteger时初始化的值,这里是0。
我们接着分析,通过上图,我们可以知道,我们调用AtomicInteger的incrementAndGet()方法,然后接着调用Unsafe的getAndAddInt()方法,getAndAddInt()这个方法,最终调用的是JDK 提供的native方法compareAndSwapInt(),这个方法就是基于CAS锁的实现,最终JVM会帮助我们将方法实现CAS汇编指令。
3.3.2 hotspot 层面
- 首先我们打开openjdk官网
OpenJDK Mercurial Repositories
- 然后找到unsafe.cpp类
jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/prims/unsafe.cpp
通过查看hostpot 源码我们可以知道,Java中Unsafe类中的native 方法:compareAndSwapInt(),最终调用到C++层面的Unsafe_CompareAndSwapInt方法,该方法最终调用了一条CPU并发原语:cmpxchg指令。它是一个原子操作,通过这个并发指令实现了共享变量的并发访问安全。
四、CAS的缺点
4.1 局限性
CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。
4.2 ABA问题
4.2.1 问题描述
比如A、B两个线程,同时对共享变量num=5进行操作,A线程操作需要花费5s,B线程需要10s,A线程先花5s时间将共享变量num修改为了6,然后再花5s将共享变量num修改回了5,此时10s的时候,B线程将共享变量修改为10,由于它之前拿到的初始值还是5,符合CAS比较交换的定义,所以能修改成功,但是其实这个值已经被其他线程修改了两次,对于这种变化,A线程是不知道的,这就是CAS的ABA问题。
4.2.2 解决方案
解决方案:数值追加版本号,使用AtomicStampedReference,在CAS时,不但会判断原值,还会比较版本信息。
4.2.2.1 代码示例
package com.ningzhaosheng.thread.concurrency.features.atom.cas;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author ningzhaosheng
* @date 2024/2/5 18:37:23
* @description 测试CAS
*/
public class TestCas {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
test_atomic_stamped();
}
public static void test_atomic_stamped() {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA", 1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
System.out.println("修改1版本的:" + b);
boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
System.out.println("修改2版本的:" + c);
}
}
4.2.2.2 测试结果
4.3 自旋时间过长问题
4.3.1 问题说明
在CAS比较交换的时候,如果修改值不成功,会一直循环尝试修改,直到成功为止,这个循环就称为自旋,而循环的时间就叫自旋时间。在高并发场景下,使用CAS方式修改共享变量会有自旋时间过长的问题,而自旋本身消耗性能。
4.3.2 解决方案
可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。
好了,本次内容就分享到这,欢迎关注本博主。如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!