美文网首页
Java 并发编程AtomicInteger中的CAS原理分析

Java 并发编程AtomicInteger中的CAS原理分析

作者: Bfmall | 来源:发表于2023-04-18 17:55 被阅读0次

    AtomicInteger简介

    java.util.concurrent.atomic.AtomicInteger

    这是一个线程安全、具有原子性操作的Integer工具类。

    AtomicInteger使用

    两个线程进行加法计算:

    public class AtomicIntegerTest {
    
        private static final String TAG = AtomicIntegerTest.class.getName();
    
        private AtomicInteger atomicInteger = new AtomicInteger();
    
        private class ThreadA extends Thread {
            int value;
            @Override
            public void run() {
                super.run();
    
                for (int i=0;i<5;i++) {
                    value = atomicInteger.incrementAndGet();
                    Log.i(TAG, "ThreadA==>run...value="+value);
                }
                Log.i(TAG, "ThreadA run finish...value="+value);
            }
        }
    
        private class ThreadB extends Thread {
            int value;
            @Override
            public void run() {
                super.run();
    
                for (int i=0;i<5;i++) {
                    value = atomicInteger.incrementAndGet();
                    Log.i(TAG, "ThreadB==>run...value="+value);
                }
                Log.i(TAG, "ThreadB run finish...value="+value);
            }
        }
    
        public void testCAS01() {
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();
            threadA.start();
            threadB.start();
        }
    
    }
    

    运行结果:

         ThreadB==>run...value=1
         ThreadA==>run...value=2
         ThreadB==>run...value=3
         ThreadB==>run...value=4
         ThreadB==>run...value=5
         ThreadB==>run...value=6
         ThreadB run finish...value=6
         ThreadA==>run...value=7
         ThreadA==>run...value=8
         ThreadA==>run...value=9
         ThreadA==>run...value=10
         ThreadA run finish...value=10
    

    上述2个线程操作AtomicInteger实现每次自增加1,最终结果为10,由此可见多线程下AtomicInteger是线程安全的。AtomicInteger如何保证线程安全,就是接下来要介绍的CAS。

    CAS简介

    在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。

    核心思想

    一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

    比较 A 与 V 是否相等。(比较) 如果比较相等,将 B 写入 V。(交换) 返回操作是否成功。 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

    image

    如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。

    如果不使用CAS机制,看看存在什么问题:

    假如V=1,现在Thread1要对V进行加1,Thread2也要对V进行加1,首先Thread1读取V=1到自己工作内存A中此时A=1,假设Thread2此时也读取V=1到自己的工作内存A中,分别进行加1操作后,两个线程中B的值都为2,此时写回到V中时发现V的值为2,但是两个线程分别对V进行加处理结果却只加了1有问题。

    CAS 实现

    本文将对 java.util.concurrent.atomic 包下的原子类 AtomicInteger 中的 compareAndSet 方法进行分析,就能发现最终调用的是 sum.misc.Unsafe 这个类。 看名称 Unsafe 就是一个不安全的类,这个类是利用了 Java 的类和包在可见性的的规则中的一个恰到好处处的漏洞。Unsafe 这个类为了速度,在Java的安全标准上做出了一定的妥协。

    看下AtomicInteger 的源码:

    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
        private static final long VALUE;
    
        static {
            try {
                VALUE = U.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (ReflectiveOperationException e) {
                throw new Error(e);
            }
        }
    
        public final boolean compareAndSet(int expect, int update) {
            //compareAndSwapInt就是所说的CAS(比较并且替换值)
            return U.compareAndSwapInt(this, VALUE, expect, update);
        }
    
        //递增方法
        public final int incrementAndGet() {
            return U.getAndAddInt(this, VALUE, 1) + 1;
        }
        
    }
    

    可以看到通过sun.misc.Unsafe实现了自增操作, Unsafe类是在sun.misc包下,不属于Java标准。
    看下Unsafe.java的getAndAddInt方法:

    /*
     * 反编译查看getAndAddInt
     */
    public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
    {
      int i;
      do
      { 
        i = getIntVolatile(paramObject, paramLong);
      } while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
      return i;
    }
    

    getIntVolatile和compareAndSwapInt都是native方法
    getIntVolatile是获取当前的期望值
    compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值有没改变,没改变就用新值直接给该内存区赋值,i为期望值即旧值,i + paramInt 为新值。

    Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。 通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。

    再往下寻找我们发现 Unsafe的compareAndSwapInt 是 Native 的方法:

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    

    也就是说,这几个 CAS 的方法应该是使用了本地的方法。所以这几个方法的具体实现需要我们自己去 jdk 的源码中搜索。

    compareAndSwapInt方法,JDK9命名为compareAndSetInt,最终调用cmpxchg方法:

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
      oop p = JNIHandles::resolve(obj);
      jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
     
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
    } UNSAFE_END
    

    cmpxchg方法:

    汇编指令判断cpu是否多核,多核则进行lock 指令操作 ,lock指令是原子性操作。

    lock指令原子性操作锁实现:内存中加缓存行锁 / 值比较大(比如跨几个缓存行)加总线锁

    inline jint Atomic::cmpxchg(jint exchange_value,
                                volatile jint* dest,
                                jint compare_value,
                                cmpxchg_memory_order order) {
      int mp = os::is_MP();
      __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                        : "=a" (exchange_value)
                        : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                        : "cc", "memory");
      return exchange_value;
    }
    
    总结一下 JAVA 的 cas 是怎么实现的:

    1.java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
    2.unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
    3.Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性

    ABA 问题

    CAS 由三个步骤组成,分别是“读取->比较->写回”。 考虑这样一种情况,线程1和线程2同时执行 CAS 逻辑,两个线程的执行顺序如下:

    时刻1:线程1执行读取操作,获取原值 A,然后线程被切换走。
    时刻2:线程2执行完成 CAS 操作将原值由 A 修改为 B。
    时刻3:线程2再次执行 CAS 操作,并将原值由 B 修改为 A。
    时刻4:线程1恢复运行,将比较值(compareValue)与原值(oldValue)进行比较,发现两个值相等。 然后用新值(newValue)写入内存中,完成 CAS 操作。

    如上流程,线程1并不知道原值已经被修改过了,在它看来并没什么变化,所以它会继续往下执行流程。对于 ABA 问题,通常的处理措施是对每一次 CAS 操作设置版本号。java.util.concurrent.atomic 包下提供了一个可处理 ABA 问题的原子类 AtomicStampedReference,具体的实现这里就不分析了,有兴趣的朋友可以自己去看看。

    ABA问题的解决办法

    1.在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
    2.atomic包下的AtomicStampedReference类:其compareAndSet方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。

    其他问题

    CAS除了ABA问题,仍然存在循环时间长开销大和只能保证一个共享变量的原子操作

    1. 循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

    2. 只能保证一个共享变量的原子操作

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。

    比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

    CAS 的应用

    1.Java的concurrent包下就有很多类似的实现类,如Atomic开头那些。 2.自旋锁 3.令牌桶限流器

    令牌桶限流器 就是系统以恒定的速度向桶内增加令牌。每次请求前从令牌桶里面获取令牌。如果获取到令牌就才可以进行访问。当令牌桶内没有令牌的时候,拒绝提供服务。我们来看看 eureka 的限流器是如何使用 CAS 来维护多线程环境下对 token 的增加和分发的。

    参考:
    https://blog.csdn.net/zhukui66/article/details/122687371
    https://www.jianshu.com/p/6d0f135a89cd

    相关文章

      网友评论

          本文标题:Java 并发编程AtomicInteger中的CAS原理分析

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