zoukankan      html  css  js  c++  java
  • 并发之atomicInteger与CAS机制

    并发之atomic与CAS自旋锁

    通过前几章的讲解我们知道i++这种类似操作是不安全的。针对这种情况,我们可能会想到利用synchronize关键字实现线程同步,保证++操作的原子性,的确这是一种有效的方法,但我们还有一种选择--AtomicInteger。

    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;
    
    
    1. Unsafe 类是用来在任意内存地址位置处读写数据,可见,对于普通用户来说,使用起来还是比较危险的。
    2. valueOffset 表示该变量在内存中的地址。
    3. value;当前类中存储的值

    方法

    两个构造方法,赋初始值,不传参数则默认为0
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    }
    
    取值和设置值

    这个值是volatile的所以是能保证可见性的

    public final int get() {
           return value;
    }
    
    public final void set(int newValue) {
            value = newValue;
    }
    
    特殊取值和赋值。

    该方法时用常规方式赋值,不能保证变量可见性。

    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
    

    既然用volatile修饰了那么常规赋值方式到底有什么用呢?
    首先volatile关键字这一章中将到

    1. volatile变量的读操作,会强制使CPU缓存失效,强制从内存读取变量。
    2. 写volatile变量,会强制刷新CPU写缓冲区,把缓存数据写到主内存。
      而这些都是通过内存屏障实现的。如果该AtomicInteger在锁中,而锁住的代码块并不会产生可见性问题,那么volatile的操作会让系统添加多个屏障。而使用lazySet()就是在不需要让共享变量的修改立刻让其他线程可见的时候,以设置普通变量的方式来修改共享状态,可以减少不必要的内存屏障,从而提高程序执行的效率

    该方法将新值设置进去的同时将旧值返回回来

    public final int getAndSet(int newValue) {
       return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    

    比较期望值并且更新该值 原子操作

    public final boolean compareAndSet(int expect, int update) {
       return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //示例
    @Test
    public void compareAndSetTest(){
        AtomicInteger a = new AtomicInteger(10);
        a.compareAndSet(10,15);
        log.info("{}",a.get());
        a.weakCompareAndSet(10,12);
        log.info("{}",a.get());
    }
    
    

    结果:
    13:42:36.391 [main] INFO atomic.AtomicIntegerApiExample - 15
    13:42:36.404 [main] INFO atomic.AtomicIntegerApiExample - 15
    可以看到第二个语句没有替换成功

    比较并且更新,和lazySet一样用于锁中
    public final boolean weakCompareAndSet(int expect, int update) {
         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
    
    原子减操作
    //先获取后再加1
    public final int getAndIncrement() {
       return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    public final int getAndAdd(int delta) {
      return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //先加后获取
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
    
    //减1操作
    public final int getAndDecrement() {
       return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    //先减1然后获取
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
    

    对数据执行方法更新。

    传入一个lambda算法,然后按照该算法更新数据。将当前数据更新为

    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            //取值
            prev = get();
            //做操作
            next = updateFunction.applyAsInt(prev);
            //原子替换
        } while (!compareAndSet(prev, next));
        return next;
    }
    //演示
    //读取atomicInteger里面的值做lambda表达式的操作。
    //
    public void updateAndGetTest(){
        AtomicInteger atomicInteger = new AtomicInteger(10);
        IntUnaryOperator updateFunction = (a)->a*5+3;
        atomicInteger.updateAndGet(updateFunction);
        assert Objects.equals(atomicInteger.get(),53);
    }
    
    两个参数负责运算。
    public final int getAndAccumulate(int x,
                                       IntBinaryOperator accumulatorFunction) {
         int prev, next;
         do {
             prev = get();
             next = accumulatorFunction.applyAsInt(prev, x);
         } while (!compareAndSet(prev, next));
         return prev;
     }
     public final int accumulateAndGet(int x,
                                       IntBinaryOperator accumulatorFunction) {
         int prev, next;
         do {
             prev = get();
             next = accumulatorFunction.applyAsInt(prev, x);
         } while (!compareAndSet(prev, next));
         return next;
     }
    
     //示例
    
    @Test
    public void getAndAccumulateTest(){
        AtomicInteger atomicInteger = new AtomicInteger(10);
        IntBinaryOperator intBinaryOperator = (a,b)->(b-a)*2;
        atomicInteger.accumulateAndGet(2,intBinaryOperator);
        System.out.println(atomicInteger.get());
    }
    
    字符串方法 取值方法
    public String toString() {
        return Integer.toString(get());
    }
    public int intValue() {
        return get();
    }
    public long longValue() {
        return (long)get();
    }
    public float floatValue() {
       return (float)get();
    }
    public double doubleValue() {
        return (double)get();
    }
    

    CAS机制

    定义

    比如自增1算法中

    public final int getAndIncrement() {
       return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    

    AndAddInt的底层逻辑是什么

    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;
    }
    

    我们看到了一个方法的调用compareAndSwap,取这三个单词的首字母即为CAS。

    他具体是怎么操作的呢。
    var1 AtomicInteger对象本身。
    var2 该对象值得引用地址。
    var4 需要变动的数量。
    var5 是用过var1 var2 找出的主内存中真实的值。

    用该对象当前的值与var5比较,如果相同 更新var5+var4并且返回true,如果不相同继续取值然后在比较,直到更新完成。
    简单的来说:

    CAS (CompareAndSwap) 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止.

    这个CAS方法是原子性的在加上该变量由于使用了volatile修饰所以也可以保证可见性和有序性,可以模拟一个锁。通过这种比较的方式可以有效的减少锁带来的性能降低的问题。

    问题

    这么做并不是十全十美的,这里有2个比较大的问题。

    并发竞争

    在并发竞争比较严重的场合会严重的增加cpu的运算量。

    在并发竞争不严重的场合中CAS的并发处理速度是远远高于锁的,但是在竞争比较严重的场合中,因为每个线程都在执行循环判断,所以会消耗大量cpu的运算能力。在这种情况下synchronized反而是一个更好的选择。

    ABA

    可能出现了一个线程修改了该数据另外一个线程又修改了回来,第三个线程并没有发现该变量变化过了。即ABA问题。
    我们先来看一个例子:

    private static AtomicInteger atomicInt = new AtomicInteger(100);
    
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
            //进行一次ABA操作
            atomicInt.compareAndSet(100, 101);
            atomicInt.compareAndSet(101, 100);
        });
        executorService.execute(()->{
            try {
                //等待ABA执行完毕
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //判断ABA向他婆娘个则替换 返回替换结果
            boolean c3 = atomicInt.compareAndSet(100, 101);
            //true
            System.out.println(c3);
        });
        executorService.shutdown();
    }
    
    

    从上述例子中可以看出atomicInt并没有意识到自己的100与原始的100有什么差别,并且被替换成功。

    举个现实中的栗子:你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水然后下无色无味的毒。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。他只知道杯子和水还是原来的样子。这是他喝下了水,中毒了。

    这里就引申出了一个问题:ABA到底会发生在什么样的数据结构中。会产生错误呢呢?

    @Test
    public void testABA(){
        AtomicInteger a = new AtomicInteger(10);
        int x = a.hashCode();
        log.info("a= {}  code={}",a.get(),x);
        AtomicInteger b = a;
        a.set(20);
        int y = a.hashCode();
        log.info("a= {}  code={}",a.get(),y);
        log.info("res={}",b.equals(a));
    }
    
    

    11:53:24.257 [main] INFO atomic.AtomicIntegerApiExample - a= 10 code=2007331442
    11:53:24.267 [main] INFO atomic.AtomicIntegerApiExample - a= 20 code=2007331442
    11:53:24.267 [main] INFO atomic.AtomicIntegerApiExample - res=true

    通过上述的例子可以看到a引用并没有变化,但是引用的对象其中的值发生了变化那么实际上他a的应用还是a的引用然而引用的对象却不是原始对象了。这就好比被"调包“了。

    当然ABA 问题,表面上上不会影响你的业务逻辑,但是在有些情况下,发生这种中途 “调包” 的事情,还是会有影响。

    如何避免ABA问题:
    通常的做法是在CAS的每一次操作上添加一个版本号。每次不仅比较引用而且需要对比版本号。当两者都相同时才能确认A就是A,而不是ABA。
    幸好JDK中提供了AtomicStampedReference用于解决ABA的问题。

    //值以及初级版本号
    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
    public static void main(String[] args) throws InterruptedException {
      ExecutorService executorService = Executors.newCachedThreadPool();
      executorService.execute(()->{
           atomicStampedRef.compareAndSet(100, 101,
                   atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
           atomicStampedRef.compareAndSet(101, 100,
                   atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
       });
       executorService.execute(()->{
            int stamp = atomicStampedRef.getStamp();
            // stamp = 0
            System.out.println("before sleep : stamp = " + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
            boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
            System.out.println(c3);        //false
    
        });
        executorService.shutdown();
    }
    
    
    

    结果
    before sleep : stamp = 0
    after sleep : stamp = 2
    false

    我们可以看到版本号由0变成了2
    结果也是不相同。可以说缓解了ABA的问题。

    总结

    首先将了AtomicInteger的API解释了一遍 然后通过了解该类时如何保证并发的,引出了CAS。然后解释了cas原理用法以及CAS的问题并演示了demo.
    这里的CAS机制是一种自旋锁。通过不停的自旋获取更新锁。

  • 相关阅读:
    CPA财务管理例题汇总
    Vulkan(1)用apispec生成Vulkan库
    Vulkan(0)搭建环境-清空窗口
    [译]可见性判断之门系统
    《资本论》核心思想
    [译]为任意网格计算tangent空间的基向量
    [译]Vulkan教程(33)多重采样
    [译]Vulkan教程(32)生成mipmap
    [译]Vulkan教程(31)加载模型
    [译]Vulkan教程(30)深度缓存
  • 原文地址:https://www.cnblogs.com/yanlong300/p/9073338.html
Copyright © 2011-2022 走看看