java基础----CAS
一.什么是cas
CAS的全称是Compare-And-Swap,他是一条CPU并发原语。
java中的CAS,都是通过unsafe类实现的,其主要的操作是,当一个线程从主内存拿到一个变量到自己工作内存,并经过计算处理,准备写回主内存的时候,会首先比对当前主内存的变量指向的内存地址里面的值,与期望值(线程一开始拿变量时,变量对应的值)是否相等,如果相等,则表示没有其他线程对这个变量操作过,随后就将要更新的值写进主内存中。假如不相等,则表示有线程修改过这个变量,则会把主内存中变量的最新值拿回去,重新做一次计算操作,以此循环。
二.cas的底层原理
下面是java atomicInteger的代码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
下面是unsafe.class中的代码(这个类是java的原生类,在jdk的rt.jar/sun/misc里面):
//第一个参数var1为给定对象,var2为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值, //var5表示期望值,var4表示要添加的数值 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
一般来说,java无法直接访问底层系统,需要通过本地native方法来访问,而unsafe相当于一个后门,基于该类可以直接操作特定的内存。
可以看出,unsafe类可以直接操作指针,根据给定的对象和内存偏移量迅速地获取到变量值。在do...while中,首先根据对象和内存偏移量拿到一个值作为期望值,然后在while的条件语句中,再一次根据对象和内存偏移量获取变量的当前值,并与期望值作出对比,如果相等,则加上var4。假如不相等,则从新获得期望值,并循环。
三.CAS的缺点
最明显的一点,CAS有可能出现一个很长的循环,假如线程一直没有写成功,那他就会一直自旋,非常消耗CPU资源。而且它只能保证一个共享变量的操作,对于多变量操作,只能加锁。
其次就是ABA问题,导致ABA问题的原因是两个线程的工作时间差距太大。例如线程a需要10秒,线程b需要2秒,它们同时从主线程中拿到x1并开始工作,在10秒中,线程b先把x1改成x2,x2改成x3。。。。最后x3又该回去x1,这时候线程a算出结果x4并对主内存中的变量进行CAS操作,通过比较期望值和现时变量的值发现是一致的,就认为这段时间里面没有其他线程对变量进行修改过,但是实际上,这个变量以及是被修改过多次了。
那么我们改如何解决ABA问题呢?
这里涉及到一个概念叫原子引用,在实现原子性的过程中,我们可以使用java 里面的atomic类,但是有时候一些数据类需要我们自己去定制,那这些类又怎么实现原子性呢。java里面有个类叫AtomicReference,是一个原子封装类,把我们自己定义的数据类传进去之后,就可以基于CAS实现原子性。下面举个例子
import java.util.concurrent.atomic.AtomicReference;
class User{
String userName;
int age;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public User(String name, int age){
this.userName = name;
this.age = age;
}
public String toString(){
return "userName: " + this.userName + ", age: " + this.age;
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User u1 = new User("z3",22);
User u2 = new User("li4", 34);
AtomicReference<User> atomicReference = new AtomicReference<User>();
atomicReference.set(u1);
System.out.println(atomicReference.compareAndSet(u1,u2) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(u1,u2) + "\t" + atomicReference.get().toString());
}
}
然后,在原子引用的基础上,延伸出了版本原子引用,就是在CAS的基础上,对主内存中的数据记录一个版本号(时间戳),这样在实现CAS的过程中,除了比对期望值和实际值是否相等之外,还会比对版本号是否有变动过,这样可以准确地知道数据究竟有没有被修改过,也可以有效地规避ABA问题。下面还是上一段代码:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) {
//ABA问题演示,最终t2线程成功修改了变量的值
/*
new Thread(()->{
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100, 200);
},"t2").start();
*/
//版本原子引用下解决ABA原子引用问题示例
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t当前版本" + atomicStampedReference.getStamp() + "\t当前值" + atomicStampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
stamp = stamp + 1;
System.out.println(Thread.currentThread().getName() + "\t当前版本" + atomicStampedReference.getStamp() + "\t当前值" + atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(101,100,stamp,stamp+1);
System.out.println(Thread.currentThread().getName() + "\t当前版本" + atomicStampedReference.getStamp() + "\t当前值" + atomicStampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t当前版本" + atomicStampedReference.getStamp() + "\t当前值" + atomicStampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(atomicStampedReference.compareAndSet(100,200,stamp,stamp+1)){
System.out.println("t4 修改成功!!!");
} else {
System.out.println("t4 修改失败!!!");
}
},"t4").start();
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t当前版本" + atomicStampedReference.getStamp() + "\t当前值" + atomicStampedReference.getReference());
}
}
运行结果:
t3 当前版本1 当前值100
t4 当前版本1 当前值100
t3 当前版本2 当前值101
t3 当前版本3 当前值100
t4 修改失败!!!
main 当前版本3 当前值100
从上面可以看到,线程t3将变量的值从100修改成101,再从101修改为100,最后t4想去将变量的值从100修改为200的时候,修改失败,因为t4一开始拿的版本号是1,最后去做写操作的时候发现版本号是3,虽然变量的值都是100,但是变量明显被人修改过了,因此修改失败。