笔记之CAS的了解
1、compareAndSet
原子性的++可以用到的是 AtomicInteger.conpareAndSet(int expect, indt update) 方法。
其底层的方法如下,该方法本质上调用的是compareAndSwapInt()方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
而compareAndSwapInt是底层封装的其他语言的方法实现
public final native boolean compareAndSwapInt(Object var1, long var2,
int var4, int var5);
- compareAndSet(int expect, int update)的参数介绍:第一个参数为拿到的期望值,如果期望值一致的话,截进行update赋值,如果期望值不一致,证明数据被修改过,返回fasle,取消赋值。
例子:
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author ql
* @version 1.0 2021/4/12
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
//比较与交换,数据进行处理的时候进行比较,如果原来的值是5的话,就改为2021,并返回true,否则就不改,返回flase
boolean b = atomicInteger.compareAndSet(5, 2021);
System.out.println(b+"\t current data:"+atomicInteger.get());
boolean c = atomicInteger.compareAndSet(5, 1024);
System.out.println(c+"\t current data:"+atomicInteger.get());
}
}
运行结果:
true current data:2021
false current data:2021
2、CAS底层原理?对Unsafe的理解
原理:比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,一直到主内存和工作内存中的值相同为止。
2.1 atomicInteger.getAndIncrement();
默认加一
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
2.2 Unsafe
-
是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
-
Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
-
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存便易地址获取数据的。
-
变量value用volatile修饰,保证多线程之间的可见性。
2.3 CAS是什么
CAS全称呼Compare-And-Swap,它是一条CPU并发原语。
他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
getAndAddInt()方法:
- 自旋锁:是指当尝试获取锁的线程失败后,不会立即阻塞,而是采用循环的方式去尝试再次获取锁,直到得到锁为止。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
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;
}
var1 AtomicInteger对象本身
var2 该对象的引用地址
var4 需要变动的数据
var5 通过var1 var2找出的主内存中真实的值
通过“compareAndSwapInt”用该对象前的值与var5比较;
如果相同,更新var5+var4并且返回true,
如果不同,继续去之然后再比较,直到更新完成。
3. CAS缺点
- 循环时间长,开销大
例如getAndAddInt方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销
- 只能保证一个共享变量的原子操作
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
- ABA问题
那么这个问题如何产生的呢?
CAS算法实现一个重要前提需要去取内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A(所以线程2是闲的蛋疼吗 QAQ),这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程没有问题
如何解决?
-
- 介绍一下原子引用。
可以通过AtomicReference来实现对类的原子性。
public class AtomicRefrenceDemo {
public static void main(String[] args) {
User z3 = new User("张三", 22);
User l4 = new User("李四", 23);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t"
+ atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t"
+ atomicReference.get().toString());
}
}
class User {
String userName;
int age;
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
-
- 时间戳的原子引用
就是在计算的时候对数据添加一个修改版本号(和乐观锁差不多)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author ql
* @version 1.0 2021/4/22
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
//设置初始值为100,并定义100位第1版本
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=====以下时ABA问题的产生=====");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "Thread 1").start();
new Thread(() -> {
try {
//保证线程1完成一次ABA操作,睡眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "Thread 2").start();
//睡眠2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=====以下时ABA问题的解决=====");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t同时获得第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("进行两次修改ABA问题");
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
}, "Thread 3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t同时获得第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
}, "Thread 4").start();
}
}
运行结果:
=====以下时ABA问题的产生=====
true 2019
=====以下时ABA问题的解决=====
Thread 3 同时获得第1次版本号1
Thread 4 同时获得第1次版本号1
进行两次修改ABA问题
Thread 3 第2次版本号2
Thread 3 第3次版本号3
Thread 4 修改是否成功false 当前最新实际版本号:3
Thread 4 当前最新实际值:100