美文网首页
线程并发--原子变量解决自增自减原子性问题

线程并发--原子变量解决自增自减原子性问题

作者: Petrel_Huang | 来源:发表于2020-04-27 01:12 被阅读0次

    前言

        线程并发问题一直都是面试的时候经常问的问题,为什么那些面试官、老总喜欢问这些问题呢,因为多线程运行起来要比快呀?那多线程就真的要比单线程快?在我看来未必,因为多线程存在上下文切换[1]、线程死锁、以及一些受限于硬件的问题。所以今天我们就面试当中的一些问题,一起来学习并解决线程并发问题。

    自增自减原子性问题

        曾经我遇到过这样的一个问题[在我刚刚毕业出来面试的时候,曾经遇到过这样的问题,你觉得在多线程的情况下i++数据安全么?我当时是这样想的,你问我肯定是需要问为什么的,如果我说安全那就没什么必要说为什么了,所以我当时的回答是不安全的,但是为什么我就回答出来了,所以回家后我就立志要这个问什么弄明白。]:你觉得在多线程的情况下i++数据安全么?
        答案是不安全的。
        现在我们一起来分析一下为什么?
        首先i++;它本身不仅仅是表面上看上去只有一个操作,实际上它是一个“读取 - 修改 - 写入”的操作,i++可以看做一下三步操作:

    • 先是将i的值从内存中拿出来;
    • 然后将i的值加1;
    • 最后将加1后的值存放到i的内存中;
      所以i++存在原子性问题[2]。

    解决方案有两个:

    • 方案1:使用synchronized加锁,使i的数据同步,但是这个方式简单是简单,比较耗资源,在多线程竞争资源的时候,加锁、释放锁,线程之间不停的切换会引起性能问题。一个线程持有锁,其它线程需要等到的这把锁之后才能执行,期间线程是被挂起,容易导致死锁发生。
    • 方案2:使用原子变量[3]AtomicInteger定义i.
      在i++原子性问题中,方案2对比起方案1更加优。

    原子变量AtomicInteger通过以下两个实现方式保证线程安全:

    1.封装了volatile修饰的变量,使内存可见
    2.方法都实现了CAS算法来实现非加锁的原子操作。

    volatile轻量级锁的原理

        volatile关键字可以保证多线程之间的数据可见性。其实就是一个线程修改的结果,另一个线程马上就能看到。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

    没有volatile修饰的变量内存分析:


    image.png

        线程从内存中读取没有volatile修饰的变量的数据,都需要经过CPU的缓存,先从主内存中读取到CPU缓存中,然后线程从CPU缓冲中读取数据
    volatile修饰的变量内存分析:


    image.png
        线程从内存中读取volatile修饰的变量的数据,直接从主内存中获取数据,不需要经过CPU缓存,这样使得多线程获取的数据都是一致的。

    volatile不能够替代synchronized,原因有两点:

    • 对于多线程,不是一种互斥关系
    • 不能保证变量状态的“原子性操作”

    所以为了保证AtomicInteger变量的安全,引入了CAS算法。

    CAS算法

    资料上面是这样描述CAS算法的:
        CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器 操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

    什么意思?我们来看看AtomicInteger类中的自增方法:

    public final int getAndIncrement() {
           for (;;) {
               int current = get();
               int next = current + 1;
               if (compareAndSet(current, next))
                   return current;
           }
     }
    

    它做了三件事情:
    1.使用get方法获取主内存中的当前value值,get方法的实现。

    public final int get() {
         return value;
    }
    

    2.当前值current 加1;
    3.比较当前值current 是否和加1之后的next值相等,如果不相等就继续下一次循环,直到current和 next相等。那么也就是主内存中的值+1成功之后结束CAS操作.compareAndSet方法如下:

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

        其中,compareAndSwap方法中调用的compareAndSwapInt方法是JNI(Java Native Interface,JAVA本地调用)的代码,借助C来调用CPU底层指令实现的,目的为了比较内存中的值this和expect是否一致,一致则将this修改为update并返回true,通过这样的方式将内存中的值修改为update。
    如果用java代码来模拟,表示如下:

    if (this == expect) {
        this = update;
        return true;
    } else {
        return false;
    }
    

        这个CAS算法确实比synchronized修饰符更为高效的解决原子性问题,但是为什么synchronized没有被取代,也就说明CAS算法并不完美,它存在以下3点问题:
    1)ABA问题:
        ABA这个名字听着有点玄乎,其实就是说内存修改了,线程都不知道。为什么出现这样的情况,我们一起来分析一下:

        场景:现在内存中的值value为10,有两个线程,线程1将value 的值修改为20,线程2将value的值修改为30。

        时间点1:线程1获取value=10
        时间点2:线程2获取value=10
        时间点3:线程2比较value和修改值是否一致 10!=30,value=30
        时间点4:线程1 比较value和修改值是否一致 30!=20,value=20

        上述的案例最终线程1将value修改为20,但是中间线程2将value修改为30,这个线程1是无法感知的。这就是我们说的ABA问题。我们可以通过AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    2)循环时间长开销大:
        getAndIncrement方法中我们可以看到它利用了死循环,如果判断一直不为true结束方法,代码就一直执行到判断为true为止,这样compareAndSwapInt一直被调用,增加了CUP 的使用率,资源开销大。

    3)只能保证一个共享变量的原子操作
        如果多个变量就无法保证原子性了,但是这个问题后面出现了AtomicReference类保证多个变量的原子性,就是将多个变量封装到一个对象中,使用对象进行CAS算法操作。

    【相关词汇】

    [1]上下文切换:
    单个CPU运行多个线程,由于时间片比较短,也就是代码运行时间,一般在几十毫秒,所以多个线程之间不停的切换,这个过程就叫做上下文切换。

    [2]原子性问题:
    原子是世界上的最小单位,具有不可分割性。比如 a=0;这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++;这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

    [3]原子变量:
    java的concurrent包下提供了一些原子类,根据修改的数据类型,可以分为 4 类。
    基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
    数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
    引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
    对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

    [4]JNI:
    JNI是Java Native Interface的缩写,它提供了很多已经用C或者C++写好的接口给java调用,使得Java代码和其他语言写的代码进行交互。

    相关文章

      网友评论

          本文标题:线程并发--原子变量解决自增自减原子性问题

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