CAS

CAS原理

CAS(Compare And Swap)是用于多线程同步的原子性指令,CAS主要通过三个值来实现,原始内存的值,给定的预期值,给定更新值,只有当原始内存的值与给定的预期值相等的情况下,才将更新值返回。

假设有两个线程A,B同时执行a=a+1

根据JMM内存模型,A,B线程分别拷贝一份副本到自己的内存中,假设此时A线程挂起,线程B对a的值进行修改,线程B首先会判断拷贝的副本值是否与共享内存的值相等,如果相等,对a进行更新,并写回主内存,此时线程A要修改a了,但是线程A发现拷贝的副本值与共享内存中的值不同,线程A会重新从共享内存中拷贝副本进行修改。

原子类

  • 基本类型:AtomicInteger AtomicLong AtomicBoolean
  • 数组类型:AtomicIntegerArray AtomicLongArray AtomicReferenceArray
  • 引用类型:AtomicReference

底层是通过sun.misc.Unsafe+CAS思想来实现的

1
2
3
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

Unsafe中的类都是native方法,它可以通过valueOffset拿到在内存中的值,然后利用一个do…while循环,判断内存中的值是否与当前的相等,如果相等则对值进行更新,并将更新后的值返回,否则,进入循环中,重新获取内存值的最新值。

1
2
3
4
5
6
public final int getAndAddInt(object o,long offset,int delta){
int v;
do{
v = getIntVolatile(o,offset);//拿到内存位置的最新值
}while(!compareAndSwapInt(o.offset,v,v+delta))//o.offset的值 == v
}

CAS缺点

  • 如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功。可能会给CPU带来负担
  • 只能保证一个共享变量的原子操作
  • ABA问题

ABA问题

假设有两个线程t1,t2一个共享变量A,线程t1,t2分别将A拷贝一份到自己的内存,这时t1先将A的值改为B

,再将B的值改为A,这时线程T2要修改值,发现符合CAS,对A的值进行更新。这就是ABA问题,线程t1对A的值进行过修改,线程t2完全忽略了这个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AtomicReference<Integer> ar = new AtomicReference<Integer>(100);
// 线程t1
new Thread(()->{
ar.compareAndSet(100, 101);
ar.compareAndSet(101,100);
},"t1").start();
// 线程t2
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("是否修改成功:"+ar.compareAndSet(100, 101)+"\t当前最新值为"+ar.get());
},"t2").start();

打印结果
是否修改成功:true 当前最新值为101

解决方法

使用原子引用+版本号控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 初始值为 100,stamp为 1
AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(100, 1);
// 线程t1执行ABA操作,版本更新
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
try {
//等待线程t2拿到版本号
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"当前版本号"+asr.getStamp());
},"t1").start();
// 线程t2修改值
new Thread(()->{
int stamp = asr.getStamp();
System.out.println("当前线程"+Thread.currentThread().getName()+"的版本号"+stamp);
try {
// 线程t2拿到版本号后,交给线程t1执行ABA操作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean res = asr.compareAndSet(100, 2019, stamp, stamp+1);
System.out.print(Thread.currentThread().getName()+"\t");
System.out.println("是否修改成功:"+res+"当前最新版本号:"+asr.getStamp()+"当前最新值:"+asr.getReference());
},"t2").start();

打印结果:

当前线程t1的版本号1
当前线程t2的版本号1
t1 当前版本号2
t1 当前版本号3
t2 是否修改成功:false当前最新版本号:3当前最新值

结论:使用使用原子引用+版本号控制可以解决ABA问题,线程t1修改两次后,版本号变化,线程t2在对值修改的时候发现版本号不对,就不会对值进行修改

文章目录
  1. 1. CAS原理
  2. 2. 原子类
  3. 3. CAS缺点
  4. 4. ABA问题
  5. 5. 解决方法
|
载入天数...载入时分秒...