CAS操作

作者: 囧囧有神2号 | 来源:发表于2018-04-30 12:27 被阅读0次

    首先什么是原子操作?

    原子本意是“不能被进一步分割的最小粒子”,而原子操作意为”不可被中断的一个或一系列操作”;

    处理器如何实现原子操作?

    1. 首先处理器会自动保证基本的内存操作的原子性:处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
    2. 总线锁保证原子性:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
    3. 缓存锁定保证原子性:锁住总线的消耗太大,于是有了缓存锁,在Lock期间处理器缓存的内存区域会被锁定,其他处理其无法更改该块内存中的数据。具体的cache一致性可以看多核处理器cache一致性技术

    以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

    JAVA如何实现原子操作?

    JAVA使用 循环CAS
    先说说悲观锁与乐观锁:

    • 悲观锁: 假定会发生并发冲突,即共享资源会被某个线程更改。所以当某个线程获取共享资源时,会阻止别的线程获取共享资源。也称独占锁或者互斥锁,例如java中的synchronized同步锁。
    • 乐观锁: 假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。

    悲观锁会阻塞其他线程。乐观锁不会阻塞其他线程,如果发生冲突,采用死循环的方式一直重试,直到更新成功。

    CAS,Compare and Swap
    CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
    JVM中的CAS操作正是利用了提到的处理器提供的CMPXCHG指令实现的;循环CAS实现的基本思路就是循环进行CAS操作直到成功为止;
    先来看看CAS在atomic类中的应用

        public final native boolean compareAndSwapObject
           (Object obj, long valueOffset, Object expect, Object update);
    
        public final native boolean compareAndSwapInt
           (Object obj, long valueOffset, int expect, int update);
    
        public final native boolean compareAndSwapLong
          (Object obj, long valueOffset, long expect, long update);
    
    

    atomic类,它们实现了对确认,更改再赋值操作的原子性;我们说线程安全,当A线程在运行时被挂起,B线程运行后再回到A,此时B的修改对A不可见,那么便会出现问题。原先我们可以用Synchronized将代码保护起来,利用排他性,但是过于笨重,于是有了CAS,直接将原先确认操作得到的值再对它进行相关计算前,先到主内存中查看是否有被改变过,有则重新获取该值。这样同样解决了上面说的问题。

    下面以AtomicInteger为例:

        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没有方法能访问底层系统,因此需要本地方法来做,Unsafe就是一个后门,被提供来直接操作内存中的数据。
    • valueOffset:变量在内存中的偏移地址,Unsafe根据偏移地址找到获取数据。
    • value被volatile修饰,保证了内存可见性。

    getAndAdd方法

        public final int getAndAdd(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta);
        }
    //---------------unsafe
        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;
        }
    

    getIntVolatile通过偏移量获取到内存中变量值,compareAndSwapInt会比较获取的值与此时内存中的变量值是否相等,不相等则继续循环重复。整个过程利用CAS保证了对于value的修改的并发安全。

    上面我们说CAS利用了处理器的CMPXCHG指令,该指令操作的内存区域就会加锁,导致其他处理器不能同时访问它,保证原子性。

    我们因为笨重放弃了synchronized,CAS是具有原子性的,于是利用循环 + CAS来确保线程安全

    CAS存在ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
    AtomicStampedReference来解决ABA问题:这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    public boolean compareAndSet(
                   V      expectedReference,//预期引用
                   V      newReference,//更新后的引用
                  int    expectedStamp, //预期标志
                  int    newStamp //更新后的标志
    
    )
    
    

    参考资料

    聊聊并发(五)原子操作的实现原理
    深入浅出 Java Concurrency (5): 原子操作 part 4
    深入浅出CAS

    相关文章

      网友评论

          本文标题:CAS操作

          本文链接:https://www.haomeiwen.com/subject/lcuzlftx.html