您的位置:首页 > 其它

【多线程系列】CAS

2016-07-29 10:25 155 查看
CAS原理: 

      通过查看AtomicInteger的源码可知, 

       `private volatile int value;

public final boolean compareAndSet(int expect, int update) { 

                    return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 

               } ` 

            通过申明一个volatile (内存锁定,同一时刻只有一个线程可以修改内存值)类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。



二、CAS(Compare and Swap)

CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,和之前的i++并没有太大区别,是的,的确在操作上没有区别,但CAS是通过硬件命令保证了原子性,而i++没有,且硬件级别的原子性比i++这样高级语言的软件级别的运行速度要快地多。虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。

从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。

  简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。


volatile变量

private volatile int value;
首先声明了一个volatile变量value,我们知道volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。
public final int get() {
return value;
}


Compare And Set

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 注意是静态的

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置
} catch (Exception ex) { throw new Error(ex); }
}

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}


  比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,Offset - value属性在内存中的位置(需要强调的是不是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。


循环设置

       现在在来看开篇提到的两个方法,我们拿incrementAndGet来分析一下其实现过程。
public final int incrementAndGet() {
for (;;) {// 这样优于while(true)
int current = get();// 获取当前值
int next = current + 1;// 设置更新值
if (compareAndSet(current, next))
return next;
}
}

循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新至,否则重试到成功为止。这里可能存在一个隐患,那就是循环时间过长,总是在当前线程compareAndSet时,有另一个线程设置了value(点子太背了),这个当然是属于小概率时间,目前Java貌似还不能处理这种情况。

CAS用作原子操作

现在CPU内部已经执行原子的CAS操作。Java5以来,你可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能。

下面是一个使用AtomicBoolean类实现lock()方法的例子:
public static class MyLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public boolean lock() {
return locked.compareAndSet(false, true);
}
}


locked变量不再是boolean类型而是AtomicBoolean。这个类中有一个compareAndSet()方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。

如果值被替换了,compareAndSet()返回true,否则,返回false。

使用Java5+提供的CAS特性而不是使用自己实现的的好处是Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性。这会使还有CAS的代码运行更快。
简单例子:

public static void main(String[] args) {

AtomicInteger atomicInteger = new AtomicInteger(55);//55
int addAndGet = atomicInteger.addAndGet(23);//78
boolean compareAndSet = atomicInteger.compareAndSet(78, 43);//true
int i = atomicInteger.get();//43
}


cas缺点

       虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题,关于ABA问题:

       有ABA问题(即在更新前的值是A,但在操作过程中被其他线程更新为B,又更新为 A),这时当前线程认为是可以执行的,其实是发生了不一致现象,如果这种不一致对程序有影响(真正有这种影响的场景很少,除非是在变量操作过程中以此变量为标识位做一些其他的事,比如初始化配置),则需要使用AtomicStampedReference(除了对更新前的原值进行比较,也需要用更新前的
stamp标志位来进行比较)。

总结: 
可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题

--完
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  多线程 cas