java CAS

在Doug Lea提供的cucurenct包 (J.U.C)中,CAS理论是实现整个java包的基石。
 

Compare and Swap

在这里,CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”(这段描述引自《Java Concurrency in Practice》)

Compare and Set

在理解了什么是 Compare and Swap 之后,理解 Compare and Set 便很容易了。Compare and Set 就是利用 Compare and Swap 实现的非阻塞的线程安全的写操作算法。它的实现过程如下:首先读取你要更改的数据的原值,然后将其和你要更新成的值作为 Compare and Swap 操作的两个参数,如果 Compare and Swap 的返回值和原值不同,便重复这一过程,直至成功。写成伪代码如下

int old;
int new;
do {
old = value.get();
new = doSomeCalcBasedOn(old)
while (value.compareAndSwap(old, new));

Compare and Set 是一个非阻塞的算法,这是它的优势。但是有一个问题就是存在这样的可能,在每次读操作之后,写操作之前,都有另外一个线程更改了原先的值,这样 Compare and Set 操作就会不停地重试。但这样的可能只存在于理论,在实际中很少发生。

Compare and Set 广泛使用在 Java 5 中的 Atomic 类中,其它的诸如 ReetrantLock、Semaphore 等的类也通过 AbstractQueuedSynchronizer 类间接地使用了 Compare and Set。
 
AtomicInteger 的方法中i++的实现:
    public final int getAndIncrement() {
for (;;) {
int current = get();//先取出当前时间点的值
int next = current + 1;
if (compareAndSet(current, next))//然后尝试赋新值,在赋值时判断这个值是否改变。
return current;
}
} public final boolean compareAndSet (int expect, int update) {
return unsafe.compareAndSwapInt( this, valueOffset, expect, update);
}

ABA问题:
CAS操作容易导致ABA问题,也就是在做a++之间,a可能被多个线程修改过了,只不过回到了最初的值,这时CAS会认为a的值没有变。
在运用CAS做Lock-Free操作中有一个经典的ABA问题:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,

例如下面的例子:

java CAS

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

java CAS

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
java CAS
 

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题

例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger ;
import java.util.concurrent.atomic.AtomicStampedReference; public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0); public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread( new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
}); Thread intT2 = new Thread( new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System. out.println(c3); // true
}
}); intT1.start();
intT2.start();
intT1.join(); //join方法保证intT1执行完毕后执行intT2
intT2.join(); Thread refT1 = new Thread( new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
}
}); Thread refT2 = new Thread( new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System. out.println(c3); // false
}
}); refT1.start();
refT2.start();
refT1.join();
refT2.join();
}
}
上一篇:.Net魔法堂:发个待附件的邮件


下一篇:Eclipse 项目导入 Android Studio 导致的乱码问题