【JavaEE】CAS操作

  • Post author:
  • Post category:java



目录


▮CAS


▮基于CAS创造出来的原子类


▪AtomicInteger:原子整型


▪自旋锁的实现


▮CAS的aba问题


▪解决方案


▮相关面试题



▮CAS

CAS全名:compare and swap。他是CPU上的一条指令。这条指令的逻辑是:


比较内存R和寄存器A的值,若相等,则交换内存R和寄存器B的值


。不过,在实际中,我们更加关注内存上的值,所以这个操作可以换种逻辑:


比较内存R和寄存器A的值,若相等,则给R赋值为B


创造出来这么个东西是为了什么呢?因为,


CAS具有原子性


,上面的比较和赋值是一步同时完成的,它只是CPU上的一条指令。一步完成又有什么作用呢?因为,


CAS能不加锁就能实现线程安全,减少了锁的开销


。要知道,在多线程中,这种比较和赋值中是很容易出现线程安全问题的,CAS的原子性,就是它最大的价值。

以下就是CAS的伪代码实现,注意是伪代码实现逻辑

boolean CAS(内存R, 寄存器A, 寄存器B) {
    if (R == A) {
        R = B;
        return true;
    }
    return false;
}

•CAS在JVM里的实现原理(博主表示看不懂)

1.java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;

2.unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;

3.Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。


▮基于CAS创造出来的原子类

CAS指令最大的价值就是它的原子性,利用这点,我们可以创造出许多带有原子性的东西。

▪AtomicInteger:原子整型



int的增大和减小操作都是非原子的


,涉及:获取值,计算值,赋予值,这三个CPU指令。这在多线程当中是容易出现安全问题的。所以,我们使用CAS,利用它的原子性来解决这个问题

•示例:后置++操作

以后置++为例,感受一下CAS的魅力。以下是代码实例,其中getAndIncrement()方法就代表着后置++操作。

class AtomicInteger {
    //赋予的值
    private int value;
    //此方法代表着后置++操作
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

这里要注意一点,这里面使用了while()循环。这是因为,在多线程环境中,oldValue刚刚得到了value的值后,value的值就可能会被其它线程所修改。此时的CAS操作也不会成功,所以使用while循环重复执行,在value值被修改后,重新赋值给oldValue。

了解了后置++原理后,简单的写个操作示例

•AtomicInteger的其它方法

(转自:http://t.csdn.cn/8l3pS)

▪自旋锁的实现

自旋锁所在的线程能围着一个锁对象一直请求,尽可能的在第一时间就拿到这个锁对象。但在判断锁对象是否有锁这方面,在多线程中是会出现问题的。因为判断对象是否有锁,涉及:取值、比较、赋值,这三个指令。


此线程可能在判断锁对象是否上锁的过程中,锁对象被其它线程上锁,而导致此线程误判,去给一个已经上了锁的对象来上锁




下面就是自旋锁实现的代码。

public class SpinLock {
    //持有锁对象的线程,为null则表示锁对象没有被上锁
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
}

可以看到,自旋锁会一直循环执行CAS,这是要消耗系统资源的。


▮CAS的aba问题

int  A = 100;

线程t1的执行逻辑是CAS(A,100,50)

线程t2的执行逻辑是CAS(A,100,50)

线程t1执行后,A就变成了50,而线程t2的CAS就会执行失败。如果有其它线程,在t1把A改成50后,又把A改成100,使得t2得以执行。线程t2这里就可能会出现BUG,因为线程t2可能不该执行这个CAS。


aba问题就是一个值从a变成了b,又从b变回了a。虽然CAS命令依旧可以执行,但这个变换过的A值,可能在逻辑上出现了一些错误


举个示例。小明卡里的余额还有100,小明想去ATM里取走50。在第一次取钱操作时,ATM卡了一下,取钱迟迟没有反应;对此,小明开始了第二次取钱操作,在他刚刚操作好第二次前,第一次取钱操作被启动了,卡里的余额只剩50,那么这第二次取钱操作就会失败。这是一个正常的情况,问题是,如果在第一次取钱操作执行后,小明的朋友又给小明卡里打了50块钱,卡里的余额又变成了100,那么第二次取钱操作就会成功执行,这下就会执行两次取款操作,跟小明的预期不符,就会产生BUG。

▪解决方案



给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期



CAS 操作在读取旧值的同时, 也要读取版本号.。真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据,并把版本号 + 1。 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了

就比如上例中,小明的两次取钱操作的版本都是:扣钱01。在第一次取钱操作后,版本变成“取钱02”,那么第二次取钱操作的版本就会对不上,从而执行失败。

在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能. 关于 AtomicStampedReference 的具体用法此处不再展开. 有需要的自行查找。


▮相关面试题

来自比特就业课的课件,做了一个搬运。



版权声明:本文为qq_63230990原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。