欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

原子操作类——CAS原理详解(二)

程序员文章站 2022-03-08 17:52:21
...

CAS你知道吗?
原子类的实现没有加锁,而是通过不停自旋进行CAS实现。(由unsafe类提供底层支持,unsafe类的方法大部分都是native方法)

谈谈原子类AtomicInteger带来的ABA问题?原子引用更新知道吗?

 

一、CAS 涉及到笔记:并发编程中的——原子类操作

什么是CAS:

CAS:compareAndSwap(比较并交换,这里的比较是将自己的期望值(即期望主内存中是这个值)与主内存中的值比较)

JUC.atomic包下有很多原子类,它们提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

Atomic包下的这些类都是采用乐观锁策略CAS(又称为无锁操作)来更新数据的。

 

之前讲volatile的时候讲过,它是轻量级同步机制,但是不保证原子性,为了解决这一问题,在中又使用了原子类,比如atomicInteger类,调用atomicInteger.getAndIncrement();     但是它的原理是什么呢?

 

编程人员写的代码:

(这串代码就解决了只用volatile所不能解决的i++在多线程下原子安全性问题)

//AtomicInteger示例:
AtomicInteger atomicInteger = new AtomicInteger(); //参数什么都不写的话,默认是0(相当于指定了主内存的初始值就是0
System.out.println(atomicInteger.getAndIncrement());

 

查看atomicInteger.getAndIncrement()的源码:

//unsafe是Unsafe类的一个实例对象
private static final Unsafe unsafe = Unsafe.getUnsafe(); 

//那么getAndIncrement是如何实现原子操作的呢?让我们一起分析其实现原理,getAndIncrement的源码如下:
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1); //getAndIncrement()中调用了unsafe对象所在类中(Unsafe类在sun.mics包中)已经封装好了的getAndAddInt(this, valueOffset, 1)方法,所以这里不重要(但是是这个方法里有do while架构,即真正使用了CAS的就是这个方法)
}

从源码发现,getAndIncrement()本质上是调用了unsafe类的getAndAddInt(this, valueOffset, 1)方法

(其中,this指当前对象atomicInteger对象,即自己new出来的实例对象,valueOffset指内存偏移量,理解成当前对象的内存地址就行了,1指"+1操作"),

所以继续点进去

 

继续点击unsafe.getAndAddInt(this, valueOffset, 1);
来到Unsafe.class文件,查看源码:

/*该方法首先通过this.getIntVolatile(var1, var2); 获取当前对象var1——atomicInteger对象指向的内存地址里内存偏移量var2的值*(即旧值);
*然后通过CAS的方式更新值为var5+var4,更新失败进入自循。
*/ 
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //从主内存拷贝值var5到自己的工作内存
        var5 = this.getIntVolatile(var1, var2); //先获取var1(即当前对象:atomicInteger对象)指向的内存地址(var2)的值var5。这一行代码也就是获取旧的值,这是Unsafe类中一个native方法,在Unsafe类中没有方法体
        
        //将自己工作内存中的值var5与主内存中的值(需要通过var1,var2再一次获得)比较,期望主内存中的值没有变,还是等于自己工作内存中的值,即希望自己工作内存中的值没有过时,好对工作内存中的值进行修改后以刷新到主内存中。
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  //再根据获取的旧值(参数3:var5),通过CAS的方式(compareAndSwap)更新为新值(参数4:var5+var4),更新失败进入自循,这也是Unsafe类中一个native方法,在Unsafe类中没有方法体   这个方法是原子的
这个方法好像和自旋有关(即do{}while()一直不成功,就会一直循环/旋转)
后面会讲一个 自旋锁:尝试获取锁的线程如果失败,不会阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    return var5;//这里返回的是旧值
}

关于do{}while()我还有话要说:(重要!!!!!!!!!!!!!!!!!!!!!!!!)
var5 = this.getIntVolatile(var1, var2);//var5是指本地内存的值,现在想要更新这个值,当然要先获取它
this.compareAndSwapInt(var1, var2, var5, var5 + var4);//这个方法会重新从根据var1 var2获取一次主内存中的期望值,将该期望值与上一步获得的var5比较,希望主内存中的这个期望值没有被其他线程改变过,还是等于自己本地内存中的var5,如果不等于,失败则更新旧值(即重新获取一次var5(因为Unsafe类中的成员变量value被volatile修饰,所以其他线程对该值的修改对该线程可见),可以理解成再一次从主内存获取最新值到自己的工作内存,再进行一次while里的判断)

/**
* 如果当前数值(主内存中的值)是自己工作内存里的期望值expected,则原子的将Java变量更新成x
* @return 如果更新成功则返回true

如果失败则更新旧值(即重新获取一次var5)
*/
//public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x );

 对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A(var5),要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

原子操作类——CAS原理详解(二)

注:t1,t2线程是同时更新同一变量56的值
因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。
(上图通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。(如举例的图所示)
当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

 

总结:

原子类操作之所以可以在多线程中不用synchronized,也能保证原子性的原因是原子类操作调用了在底层的Unsafe类的方法(这些方法采用CAS策略)。

(之所以说Unsafe类在底层,是 因为Unsafe类中的方法是native修饰的,也就是说sun.mics.Unsafe类中的方法,像C的指针一样直接操作指定内存(valueOffset),直接调用操作系统底层资源执行相应任务)。

根据上面,可以说原子类操作的核心是Unsafe类,Unsafe类的核心是CAS(compare and swap)策略。而CAS底层又和什么汇编语言有关...

 

Unsafe类

原子操作类——CAS原理详解(二)

 

而valueOffset在原子类中,又是以类常量形式存在: 

原子操作类——CAS原理详解(二)

 

CAS
关于CAS再理解:

原子操作类——CAS原理详解(二)

 

什么是CAS?为什么用CAS而不用synchronized?
答:CAS和synchronized比,用了一种折中的方案,允许并发循环反复尝试修改。synchronized太重了,而CAS不加锁,多个线程可以通过do{}while()反复多次循环来达到线程安全的操作值的效果。 

CAS的缺点是什么?
1.有可能长期do{}while()循环,给CPU带来很大开销;
2.只能保证一个共享变量的原子操作;???为什么???因为从cas方法中看出,它只能比较一个旧值(期望值)和一个内存真实值,即一对,而非两对
3.ABA问题。

原子操作类——CAS原理详解(二)

how to solve CAS 带来的ABA问题:原子类不用AtomicInteger或AtomicReference,而用AtomicStampedReference(值+版本号),在使用CAS更新时参数不再是两个(期望值,修改值),而是(期望值,修改值,期望版本号,修改版本号) 

AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.compareAndSet(1,2);

AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<Integer>(1,1);
integerAtomicStampedReference.compareAndSet(1,2,1,2);

原子操作类——CAS原理详解(二)

 

另外,atomicReference可以实现    多线程间通过自旋锁竞争资源的方案。(即资源类利用atomicReference来实现lock、unlock()方法)
即用AtomiReference代替lock??待补充