Java中的CAS操作

2020-02-24  本文已影响0人  凉风拂面秋挽月

问题

volatile关键字解决了内存可见性的问题,但是不能解决线程中各工作内存不一致的情况,正如深入理解jvm虚拟机中所言:
关于volatile变量的可见性,经常被开发人员误解,认为以下描述成立:“volatile变量对所有线程是立即可见的,对volatile变量的所有读写操作都能立即反应到其他线程之中,换句话说,volatile在各个线程中是一致的,所以基于volatile变量的运算在并发条件下是安全的。”
该句话的论据无误,但是论据不能推出结论,由于java运算并非原子性操作,导致volatile变量在并发条件下一样是不安全的。
如书中的这种场景:一个被volatile修饰的变量,同时被20各线程累加,每个线程累加10000次,最后当所有线程执行完,打印该变量的值为小于200000的某值。原因就在于变量的累加上:race++
根据javac反编译得到的结果,race++由4条字节码指令构成,getstatic(入栈放入栈顶)iconst_1(出栈)iadd(加1)putstatic(放回栈顶)
volatile可以保证getstatic时数据是最新的,但是执行后面的指令时,数据有可能被已经其他线程更改了,如t1线程和t2线程同时读取到56,做自增,t2快了一步,自增写入内存完成,t1才做iadd,这时候的iadd已经是无意义了,应该让t1重新去getstatic,但是t1并不知道这些还是对56+1,然后刷新回内存,用57去替换内存中的57。

为了解决该问题,可以采用加锁的方式,保证i++类似的操作的原子性,但由于锁的开销大,在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题 一个线程持有锁会导致其他所有需要此锁的线程挂起 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

独占锁是一个悲观锁,synchronized就是一种独占锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一种更加有效的锁就是乐观锁,CAS就是一种乐观锁。

什么是CAS

在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量支持并发操作的原语(原语是由若干条指令组成的,用于完成一定功能的一个过程,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断),从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。
在java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统性能的。而随着java不断的发展,java本地方法(JNI)的出现,为java程序越过jvm直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的concurrent包中,CAS理论是他实现整个java并发包的基石。
CAS操作包含三个操作数-- 内存位置的值(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

我们以CompareAndSetInt为例说明问题,
o:对象的内存位置
offset:对象中变量的偏移量
expected:变量的预期值
x:新的值

public final boolean CompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
        return compareAndSetInt(o, offset, expected, x);
    }

其操作含义是如果对象obj中内存偏移量为offset的值为expected,则用新的值替换expected。

我们用cas操作来解决上面代码中volatile无法解决的问题。
CAS比较与交换的伪代码可以表示为:

do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
image.png

线程t1和线程t2同时相对内存中的变量+1,就以上面的volatile例子,都想自增100次,那么t2拿到的是t1自增过的值,所以不会自增成功,于是重新去获得当前内存中最新的值继续执行cas。如果t1一直比t2执行的块,那么cas操作会让t2失败100次,直到t1执行完,t2才能执行cas成功。
这样就不会出现多线程对变量累加,最终结果比预期小的情况。

容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
CAS是一种有名的无锁算法。无锁编程,即不适用锁的情况下实现多线程之间的变量同步,也就是在没有现成被阻塞的情况下实现变量的同步。

应用

在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

image.png
上一篇下一篇

猜你喜欢

热点阅读