Java多线程与并发_CAS详解
真正能让你走远的,都是自律、积极和勤奋
一、什么是CAS?
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存与工作内存中的值一致
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019) + "\t current data: " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,1024) + "\t current data: " + atomicInteger.get());
}
}
结果
true current data: 2019
false current data: 2019
Process finished with exit code 0
原子整型类设置初始值为5,主存中共享变量值为5,某线程在自己的工作内存进行修改操作值变为2019,在刷新回主内存时,与原先值进行比较,发现还是5,则将主存中值修改为2019,成功;很快第二个线程也想将刚刚操作完的值2014刷回主存,但经过比较,发现值已经发生了变化,不是最初的5而是变成了2019,所以修改失败!
二、原子操作类getAndIncrement()方法详解
atomicInteger.getAndIncrement()方法调用了Unsafe类的getAndAddInt()方法
/**
* Atomically increments by one the current value.
*
* @return the previous value
* this :当前对象
* valueOffSet :内存偏移量
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
1.Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc中,其内部方法操作可以像C指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
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找出的主内存中真实的值
- 用该对象当前的值与var5比较,如果相同,更新var5+var4并且返回true;如果不相同,继续取值然后再比较,直到更新完成
2.原理
1.AtomicInteger里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存。
2.线程A通过getIntVolatile(var1,var2)拿到value值5,这时线程A被挂起。
3.线程B也通过getIntVolatile(var1,var2)方法获取到value值5,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。
4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字5与主内存值6不一致,说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新再来一遍。
5.线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法比较,直到成功。
*为什么用CAS不用synchronized?
加锁,同一时间段只能有一个线程来访问,一致性得到了保证,但是并发性下降
3.底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
先想办法拿到变量value在内存中的地址
通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值
三、CAS的缺点
1.循环时间长开销大:如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
2.只能保证一个共享变量的操作:对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用加锁来保证原子性
3.ABA问题:(狸猫换太子)如果一个值原来是A,变成B,又变成A,检查时发现它的值没有变化。但实际发生了变化。解决思路是使用版本号,每次变量更新的时把版本号加1,就会变成1A–2B–3C。
AtomicInteger
CAS —> Unsafe —> CAS底层思想 —> ABA —> 原子引用更新 —> 如何规避ABA问题
四、ABA问题
如果一个值原来是A,变成B,又变成A,检查时发现它的值没有变化。但实际发生了变化。
/**
* @Author: slx
* @Date: 2019/4/17 17:27
* 时间戳的原子引用,加版本号
*/
public class ABADemo { //ABA问题解决 AtomicStampedReference
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
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);
},"thread1").start();
new Thread(() -> {
try {
//暂停t2线程1秒钟,保证上面的t1线程完成了一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2019) + "\t" + atomicReference.get());
},"thread2").start();
//暂停一会线程
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第一次版本号:" + stamp + "\t值为:" + atomicStampedReference.getReference());
//暂停t3线程1秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,1,atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp() + "\t值为:" + atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp() + "\t值为:" + atomicStampedReference.getReference());
},"thread3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp + "\t值为:" + atomicStampedReference.getReference());
//暂停t4线程3秒钟,保证上面的t3线程完成了一次ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1) + "\t" + atomicStampedReference.getStamp());
},"thread4").start();
}
}
结果
---------------以下是ABA问题的产生------------------
true 2019
---------------以下是ABA问题的解决------------------
thread3 第一次版本号:1 值为:100
thread4 第一次版本号:1 值为:100
thread3 第二次版本号:2 值为:101
thread3 第三次版本号:3 值为:100
false 3
Process finished with exit code 0