前言:日常编码过程中,基本不会直接用到 CAS 操作,都是通过一些 JDK 封装好的并发工具类来使用的,在 java.util.concurrent 包下。但是在阅读源码过程中,经常会遇到 CAS。
CAS (Compare And Swap,比较与交换) ,底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)。
CAS 可以保证更新操作的原子性,通过借助 volatile 读取到共享变量的最新值来实现【比较并交换】的效果,由于内存位置的值与预期原值可能不一样,导致操作失败,为了保证操作的成功,可以使用自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止。
即无锁的原理为:CAS + 自旋 + volatile 变量
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
上下文切换成本比较高,需要保存线程的信息,下次线程从阻塞状态恢复到运行状态,需要进行信息的恢复。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景。
JUC 并发包提供了:AtomicBoolean,AtomicInteger,AtomicLong
以 AtomicInteger 为例
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
存在 ABA 问题
public class TestABA {
static AtomicReference ref = new AtomicReference("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
String prev = ref.get();
other();
TimeUnit.SECONDS.sleep(2);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
public static void other(){
new Thread(() -> {
log.debug("change A->B {}",ref.compareAndSet(ref.get(),"B"));
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
log.debug("change B->A {}",ref.compareAndSet(ref.get(),"A"));
},"t2").start();
}
}
输出
19:30:15.648 [main] DEBUG c.TestABA - main start...
19:30:15.688 [t1] DEBUG c.TestABA - change A->B true
19:30:16.689 [t2] DEBUG c.TestABA - change B->A true
19:30:18.692 [main] DEBUG c.TestABA - change A->C true
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。
解决 ABA 问题
public class SloveABA {
static AtomicStampedReference ref = new AtomicStampedReference("A",1);
public static void main(String[] args) throws InterruptedException {
log.debug("main start ...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
TimeUnit.SECONDS.sleep(2);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
public static void other(){
new Thread(() -> {
log.debug("A -> B {}",ref.compareAndSet(ref.getReference(),
"B",ref.getStamp(),ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
log.debug("B -> A {}",ref.compareAndSet(ref.getReference(),
"A",ref.getStamp(),ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
},"t2").start();
}
}
输出
19:33:28.584 [main] DEBUG c.SloveABA - main start ...
19:33:28.586 [main] DEBUG c.SloveABA - 版本 1
19:33:28.619 [t1] DEBUG c.SloveABA - A -> B true
19:33:28.619 [t1] DEBUG c.SloveABA - 更新版本为 2
19:33:29.628 [t2] DEBUG c.SloveABA - B -> A true
19:33:29.628 [t2] DEBUG c.SloveABA - 更新版本为 3
19:33:31.630 [main] DEBUG c.SloveABA - change A->C false
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了
AtomicMarkableReference
public class Test2 {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
AtomicMarkableReference ref = new AtomicMarkableReference(bag, true);
log.debug("main start ...");
GarbageBag prev = ref.getReference();
log.debug(prev.getDesc());
new Thread(() -> {
log.debug("打扫卫生的线程 start ...");
bag.setDesc("空垃圾袋");
while (!ref.compareAndSet(bag,bag,true,false)){
};
log.debug(bag.toString());
},"t1").start();
TimeUnit.SECONDS.sleep(1);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}
class GarbageBag {
private String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "GarbageBag{" +
"desc='" + desc + '\'' +
'}';
}
}
输出
19:39:32.139 [main] DEBUG c.Test2 - main start ...
19:39:32.141 [main] DEBUG c.Test2 - 装满了垃圾
19:39:32.175 [t1] DEBUG c.Test2 - 打扫卫生的线程 start ...
19:39:32.175 [t1] DEBUG c.Test2 - GarbageBag{desc='空垃圾袋'}
19:39:33.179 [main] DEBUG c.Test2 - 主线程想换一只新垃圾袋?
19:39:33.179 [main] DEBUG c.Test2 - 换了么?false
19:39:33.179 [main] DEBUG c.Test2 - GarbageBag{desc='空垃圾袋'}
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
/*
参数1 提供数组、可以是线程不安全数组或线程安全数组
参数2 获取数组长度的方法
参数3 自增方法,回传 array, index
参数4 打印数组的方法
*/
public static void demo(
Supplier arraySuppiler, // Supplier T get();
Function lengthFun, // Function R apply(T t)
BiConsumer putConsumer, // BiConsumer void accept(T t, U u)
Consumer printConsumer // Consumer void accept(T t)
){
List ts = new ArrayList<>();
// 获取数组
T array = arraySuppiler.get();
//获取数组长度
int length = lengthFun.apply(array);
for (int k = 0; k < 5; k++) {
ts.add(new Thread(() -> {
//对数组的每个下标做2000次自增运算
for (int i = 0;i < 2000;i++) {
for (int j = 0; j < length; j++) {
putConsumer.accept(array, j);
}
}
}));
}
// 启动所有线程
ts.forEach(t -> t.start());
// 等所有线程结束
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
printConsumer.accept(array);
}
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
输出:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
public class Test1 {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater field = AtomicIntegerFieldUpdater.newUpdater(Test1.class, "field");
Test1 test1 = new Test1();
field.compareAndSet(test1,0,10);
System.out.println("修改后的field: " + test1.field);
field.compareAndSet(test1,10,20);
System.out.println("修改后的field: " + test1.field);
field.compareAndSet(test1,10,30);
System.out.println("修改后的field: " + test1.field);
}
}
输出:
修改后的field: 10
修改后的field: 20
修改后的field: 20
以AtomicInteger 类的 getAndIncrement()方法源码为例
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long valueOffset; //内存偏移量
private static final Unsafe unsafe = Unsafe.getUnsafe(); //给Unsafe类的初始化,方便方法中调用。
static {
try {
//给valueOffset初始化
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
private volatile int value;
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
valueOffset 是什么?
valueOffset 所代表的是AtomicInteger对象的value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存当前值。也就是说,valueOffset 是当前AtomicInteger对象初始化时的原始值的内存地址。例如:AtomicInteger atomicInteger = new AtomicInteger(5); 这个5就是原始值,即valueOffset 是5的内存地址。
CAS + 自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 由于 value 声明为 volatile,所以以这种方式读取
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
// 在对象指定偏移地址处 volatile 读取一个int
public native int getIntVolatile(Object var1, long var2);
结论:AtomicInteger 通过 CAS + 自旋 + volatile value 保证自增方法成功执行。
1、循环时间长导致CPU开销大
在并发量比较高的情况下,自旋CAS(不成功就一直循环执行,直到成功) 如果长时间不成功,会给CPU带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
2、只能保证一个共享变量的原子操作
CAS机制保证的只有一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证多个变量共同进行原子性的更新,循环CAS就无法保证操作的原子性,就不得不使用Synchronized了。
或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
3、CAS带来的ABA问题
ABA问题就是,假如Thread1和Thread2先后从内存中拿到值A。这时,Thread1把内存中的值A改为了值B,突然又来了个Thread3,它拿到内存中的值B后,然后又将内存中的值B改为了值A。这过程中,Thread2全然不知。 尽管Thread2的CAS操作成功,但是不代表这个过程就是没有问题的。
ABA问题解决:在JDK中有就提供了java.util.concurrent.atomic.AtomicStampedReference类。AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化。
CAS 适用于读多写少的情况下,这样冲突一般较少;而synchronized 适用于写多读少的情况下,冲突一般较多。
参考文章:(2条消息) CAS机制专篇,超详细讲解!(比较与交换,compareAndSwap,CAS原理,Unsafe是什么?valueOffset是什么?CAS的缺点,CAS的应用场景,ABA问题,ABA问题的解决办法)_程序员不相信秃头的博客-CSDN博客_valueoffset