在java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue
类的Xfer方法。CAS虽然很高效地解决了原子操作,但是CAS仍然存在三大问题ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
ABA问题
在CAS操作值的时候,如果一个变量是A,变成了B,又变成了A,那么使用CAS进行检查时是发现没有变化,但是的确是变化了。解决思路加一个版本号去做标识,有点像线程里的信号量一样,在每次变量发生更新时是版本号+1,那么A–>B–>A 变成 1A->2B->3A,幸运的是咋java1.5开始,JDK的atomic包里里提供了AtomicStampedReference
类来解决,这个类compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设定为给定的更新值。
/**
* Atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,//预期引用
V newReference,//更新后引用
int expectedStamp,//预期标志
int newStamp) {//更新后标志
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。
pause指令有两个作用:
- 延迟流水线执行指令de-pipline,使CPU不会消耗过多的执行资源,延迟的时间取决与具体实现的版本,在一些CPU上延迟时间是0
- 避免在退出循环的时候因内存顺序冲突引起CPU流水线清空,从而提高CPU执行效率。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环cas(自旋锁)的方式保证原子性,但是对多个共享变量操作时,循环CAS就无法保证操作原子性,这时后可以取用锁,,也可以取巧把多个共享变量合并成一个共享变量操作。
从java1.5时,JDK提供了AtomicReference类保证引用对象之间的原子性。