1. 简介
CAS
,Compare And Swap
,即比较并交换。整个AQS
同步组件、Atomic
原子类操作等等都是以CAS
实现的,甚至ConcurrentHashMap
在1.8
的版本中也调整为了CAS
+Synchronized
。可以说CAS
是整个JUC
的基石。
2. CAS 分析
在CAS
中有三个参数:内存值V、旧的预期值A
、要更新的值B
,当且仅当内存值V
的值等于旧的预期值A
时才会将内存值V
的值修改为B
,否则什么都不干。其伪代码如下:
if(this.value == A){
this.value = B
return true;
}else{
return false;
}
JUC
下的atomic
类都是通过CAS
来实现的,下面就以AtomicInteger
为例来阐述CAS
的实现。如下:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
Unsafe
是CAS
的核心类,Java
无法直接访问底层操作系统,而是通过本地(native
)方法来访问。不过尽管如此,JVM
还是开了一个后门:Unsafe
,它提供了硬件级别的原子操作。valueOffset
为变量值在内存中的偏移地址,unsafe
就是通过偏移地址来得到数据的原值的。value
当前值,使用volatile
修饰,保证多线程环境下看见的是同一个。
我们就以AtomicInteger的addAndGet()
方法来做说明,先看源代码:
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
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;
}
内部调用unsafe
的getAndAddInt
方法,在getAndAddInt
方法中主要是看compareAndSwapInt
方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);
该方法为本地方法,有四个参数,分别代表:对象、对象的地址、预期值、修改值。
CAS
可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。
CPU
提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。
1、总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#
信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU
和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。所以就有了缓存加锁。
2、缓存加锁:其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#
信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1
修改缓存行中的i
时使用缓存锁定,那么CPU2
就不能同时缓存了i
的缓存行。
3. CAS 缺陷
CAS
虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
循环时间太长:如果CAS
一直不成功呢?这种情况绝对有可能发生,如果自旋CAS
长时间的不成功,则会给CPU
带来非常大的开销。在JUC
中有些地方就限制了CAS
自旋的次数,例如BlockingQueue的SynchronousQueue
。
只能保证一个共享变量原子操作:看了CAS
的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS
也不错。例如读写锁中state
的高低位
ABA问题 :CAS
需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A
,变成了B
,然后又变成了A
,那么在CAS
检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA
问题。对于ABA
问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1
,即A —> B —> A,变成1A —> 2B —> 3A。
CAS
的ABA
隐患问题,解决方案则是版本号,Java提供了AtomicStampedReference
来解决。AtomicStampedReference
通过包装[E,Integer]
的元组来对对象标记版本戳stamp
,从而避免ABA
问题。
AtomicStampedReference
的compareAndSet()
方法定义如下:
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)));
}
compareAndSet
有四个参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。源码部分很好理解预期的引用 当前引用,预期的标识 当前标识,如果更新后的引用和标志和当前的引用和标志相等则直接返回true
,否则通过Pai
r生成一个新的pair
对象与当前pair CAS
替换。
Pair
为AtomicStampedReference
的内部类,主要用于记录引用和版本戳信息(标识),定义如下:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
Pair
记录着对象的引用和版本戳,版本戳为int
型,保持自增。同时Pair
是一个不可变对象,其所有属性全部定义为final
,对外提供一个of
方法,该方法返回一个新建的Pari
对象。pair
对象定义为volatile
,保证多线程环境下的可见性。在AtomicStampedReference
中,大多方法都是通过调用Pair
的of
方法来产生一个新的Pair
对象,然后赋值给变量pair
。如set
方法:
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}