一》对于CAS的理解
要对CAS进行探究,我们先从AtomicInteger这个类的getAndIncrement()这个方法说起 ,这个方法主要可以解决volatile关键字不保证原子性的问题。下面我们进入这个方法中进行进行探究:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
进入getAndIncrement()这个方法,可以看到底层调用了Unsafe这个类对象的getAndAddInt()方法,再进入Unsafe这个类:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
public native Object getObjectVolatile(Object var1, long var2);
public native void putObjectVolatile(Object var1, long var2, Object var4);
Unsafe这个类来自JVM里rt.jar这个包,是JVM自身携带的一个类,可以看到里边的方法都是native关键字修饰的,说明Unsafe这个类的方法都是调用操作系统底层的资源来执行相应的任务的,类似于C语言中的指针操作内存。getAndIncrement()这个方法之所以能保证原子性,就是Unsafe这个类起到了作用。
再进入getAndAddInt(this, valueOffset, 1)这个方法:
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;
}
这段代码就是CAS操作的核心,可以看到主要是通过循环来实现的,先来看看几个核心参数:
var1:表示当前的对象;
var2 :表示地址偏移量(Unsafe就是通过地址偏移量来获取数据的);
var5 :表示主物理内存中真实的共享变量的值;
var4+var5:表示变化量
首先通过var1和var2来获取主物理内存中的真实值var5,再与当前对象的值进行比较,如果相等,更新var4+var5,返回true,跳出while循环,如果不相等,继续进行取值和比较,直到相等为止。
既然Unsafe这个类是通过偏移量进行获取数据的,那么这个偏移量是怎么计算的呢?
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
可以看到底层的value值用volatile关键字修饰,这样就保证了共享变量的可见性。
讲到这里,那么什么是CAS呢,所谓的CAS,就是一个比较并交换的过程,它是通过自旋锁的方式进行线程安全的保证。它先将预期值和真实值进行比较,看是否相等,相等的话,就进行修改,这个过程是原子性的。CAS是一条并发原语,是CPU的原子指令,依赖于硬件的物理功能,执行过程不会被打断,就不会存在数据不一致的问题。
讲到这里,大家可能还是不太懂,下面就通过一个具体的实例来体会CAS的原理:
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
//先将主物理内存中的5改为2019
boolean b = atomicInteger.compareAndSet(5, 2019);
//再将主内存中的值改回5
boolean b1 = atomicInteger.compareAndSet(5, 5);
System.out.println(b+"\t"+atomicInteger.get());
System.out.println(b1+"\t"+atomicInteger.get());
}
运行结果:
主物理内存里的共享变量的值为5,调用compareAndSet()方法进行比较并交换,通过比较,主物理内存中的值和期望的值是相等的,这时候交换成功,返回true,主物理内存中的值被换成2019。但是当重新改回5的时候,返回是false,说明修改失败,主内存中的值依然是2019,下面通过图示的方法分析一下这个修改失败的原因。
先来对这个JMM内存模型进行一个描述:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
第一次修改之后,主内存中的共享变量的真实值变成了2019,当第二次想改回来的时候,将期望值5和主内存中的值2019进行比较,发现二者不相等,所以修改失败。
二》CAS的缺点
CAS虽然能保证线程安全性和并发性,但还有缺点:
1.自旋的时间过长,消耗资源;
2.只能保证一个共享变量的原子性;
3.会产生ABA问题
三》ABA问题
ABA问题就是,一个线程修改了主物理内存中的值,又将值改为原来的值,这时候另一个线程来进行比较,以为值没有被修改,但实际被修改过,类似于狸猫换太子。
比如线程T1执行时间需要10s,线程T2执行时间是2s,线程T2比线程T1快,加入主物理内存中的共享变量的值是A,T2经过比较,跟自己的工作内存中的值是一致的,这时候就把值改为B并放到主物理内存中,由于T2执行速度快,这时候又把值改为A,这时T1线程执行,自己工作内存中的值和主物理内存中的值相同,以为没被改过,但实际上被动过,类似于狸猫换太子
在学习ABA问题之前,我们先来了解一下原子引用。
在前面我们提到了原子整形AtomicInteger,它放进主物理内存的值只能是整形,但是当我们想把自定义对象放进主物理内存,就要用到原子引用。
public static void main(String[] args) {
User user=new User(1,"zhangsan");
AtomicReference atomicReference=new AtomicReference<>();
//将user对象放进了主物理内存
atomicReference.set(user);
System.out.println(atomicReference.get());
运行结果:
可以看到,通过原子引用,我们成功的把自定义的对象放进了主物理内存。
那么ABA问题该怎么解决呢?这里我们需要用到版本号原子引用AtomicStampedReference。
定义一个版本号,版本号的作用就是,当线程对主物理内存中的值改变时,每改变一次,就让版本号加1,当发生ABA问题,另一个线程进行比较的时候,虽然主物理内存中的值和期望值一致,但是版本号不一致,这时,就不能成功修改,防止了ABA问题。
下面看一个实例:
//版本号默认是1,主内存的值是100
AtomicStampedReference atomicStampedReference=new
AtomicStampedReference<>(100,1);
//创建线程a
new Thread(
()->{
int tamp=atomicStampedReference.getStamp();//初始版本号
System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);
//暂停一秒,让b线程读取初始版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//让a线程进行一次ABA操作
atomicStampedReference.compareAndSet(100,
101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());
//在将2019改回100,版本号+1
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"第三次版本号为: "+atomicStampedReference.getStamp());
},"a"
).start();
//创建线程b
new Thread(()->{
int tamp=atomicStampedReference.getStamp();//初始版本号
//暂停4秒,让a线程进行一次ABA操作
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);
//b线程进行值的修改,版本号+1
atomicStampedReference.compareAndSet(100,
2019,tamp,atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.getReference());
},"b").start();
}
运行结果:
b线程的期望版本号是2,因为修改了一次,但是a线程进行ABA操作,修改了两次,实际版本号是3,版本号不相同,b线程修改值失败,依然是100。