在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
一、CAS机制介绍
CAS:英文名Compare And Swap的缩写,意为比较并交换。
CAS机制:当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试,也可以说是无锁。
加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。
无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突为止。CAS正是无锁实现的一种方式。
CAS 操作函数结构:CAS(V,A,B)
其中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”
二、Java中CAS的支持
在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
以 java.util.concurrent 中的 AtomicInteger 为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 getAndIncrement 方法,该方法的作用相当于 i++ 操作。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
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;
public final int get() {
return value;
}
//expect期待的值,update将要更新成的值,this是对象自己,valueOffset是对象地址
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//同上
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
getAndIncrement()方法调用了getAndAddInt()方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//根据偏移量var2获取对象var1整型field的值,赋给var5
var5 = this.getIntVolatile(var1, var2);
//通过CAS判断整型field的值是不是var5得知是否被别人修改,如果没有,就更新
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
getAndAddInt()方法中有三个参数:
Object var1:是AtomicInteger对象自己本身,即操作的对象。
long var2:是AtomicInteger对象中整型field的偏移量,通过这个偏移量,即得到了对象整型field的值。
int var4:是增加的量,此方法为自增1,因此这个值为1。
getIntVolatile()方法获取了对象整型值,通过CAS进行判断,这里的compareAndSwapInt()方法,即为CAS方法的实现,调用的native底层方法。
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int x);
底层调用的C++方法,在进行操作的时候,会在指令前面加上lock前缀。
三、CAS的缺点
1、ABA问题
比如说一个线程t1从内存位置V中取出A,这时候另一个线程t2也从内存中取出A,并且t2进行了一些操作变成了B,然后t2又将V位置的数据变成A,这时候线程t1进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。如下所示:
现有一个用单向链表实现的堆栈,栈顶为A,这时线程t1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
于是调用head.compareAndSet(A,B);
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于期待的引用,再检查当前标志是否等于期待的标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean compareAndSet(
V expectedReference,//期待的引用
V newReference,//更新后的引用
int expectedStamp, //期待的标志
int newStamp //更新后的标志
)
实际应用代码:
private AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
........
atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
就在判断值上又加了一层类似时间戳标志位的判断。
2、循环时间长开销大
自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
CAS与Synchronized的使用情景:
1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
四、总结
concurrent包的实现都基于volatile关键字和CAS操作,volatile关键字实现了变量的可见性,CAS操作保证了操作的原子性,这两个操作合起来,就构成了concurrent包得以实现的基石。
即:
1. 首先,声明共享变量为volatile;
2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
从整体来看concurrent包的实现,引用一张图来解释压轴:
因此,学习和理解了本文讲解的内容,就能更加容易的理解concurrent包里的内容,和设计的原理。