zoukankan      html  css  js  c++  java
  • AtomicInteger原理分析

      AtomicInteger是对Integer类型的一个包装,提供原子性的访问和更新操作。其原子性的操作是基于CAS实现的。CAS的过程是这样,执行运算时,使用当前数据值作为判断条件,利用CAS指令试图进行更新。更新之前获取内存中的最新值,与传来的当前值作比较。如果数值没有变,则说明没有其他线程进行并发修改,更新操作成功。则否则要么进行重试,要么返回结果。

    应用场景

      AtomaticInteger最典型的应用场景是计数。比如我们要统计并发插入10万条数据的耗时,我们需要对插入的数据计数,普通的int变量在多线程环境下的++操作,是线程不安全的,前一个操作可能会被后一个操作所覆盖,所以统计的技术往往小于准确值。这时候就可以使用AtomaticInteger。使用非常简单:

    private AtomicInteger counter = new AtomicInteger(0);//初始计数为0
    // doSomething,执行操作之后,计数即可
    int count = counter.incrementAndGet();

    源码分析

      基本属性

    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;
    ublic final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    } 

      可以看到,AtomicInteger的操作基本都依赖于unsafe提供的底层支持。Unsafe 会利用 value 字段的内存地址偏移,完成操作。进入Unsafe源码:

    public final int getAndSetInt(Object var1, long var2, int var3) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);//当前线程这个时刻,根据AtomicInteger对象和value的内存地址偏移,获取到value值
        } while(!this.compareAndSwapInt(var1, var2, var5,var3));
        //while条件compareAndSwapInt是CAS操作,,如果当前值和执行操作前的最新值一致,则将value加1,否则操作失败,返回false,继续获取最新值,直到更新操作成功。
        return var5;
    }
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

      可以看到Unsafe的compareAndSwapInt是用native,native 关键字告诉编译器(其实是JVM)调用的是该方法在外部定义,实际是使用C语言实现的。这里就不做深究了。

    CAS操作的副作用

      常用的失败重试机制隐含着一个假设,就是竞争情况是短暂的。在多数场景中,重试发生1到2次就成功了,但总有意外情况。所以有需要的时候,考虑自旋的次数,超过多少次之后就不再重试,避免过度消耗CPU.还有一个就是著名的ABA问题。CAS是在更新时比较前值,如果前值恰好和最新值相同(不是逻辑上的相同),例如期间发生了A->B->A的更新,可能导致不合理的操作。对于这种情况ava 提供了 AtomicStampedReference类,通过为引用建立版本号的方式,保证CAS的正确性。

    如何保证重置后的数值准确性

      假设有这样一个需求,统计每次10w条插入数据的耗时,计数到10w之后,就需要重置为0,先看下代码:

    private AtomicInteger counter = new AtomicInteger(0);//初始计数为0
    private long lastTime = 0;
    public void insert(){
      //insert
        if(counter.incrementAndGet() == 100000) {
            counter.set(0);
            long currentTime = System.currentTimeMillis();
            log.info("\n\n=============== insert 10w data,time="+ currentTime+",used"+(currentTime-lastTime)+"'s ================\n\n");
            lastTime = currentTime;
      }
    }

      counter.incrementAndGet()的值大于10w时,我们使用set方法,将value值重新置为0。多线程环境下,可能出现多个线程同时执行counter.incrementAndGet()这句代码(还没有执行它的返回值==10w的判断),第一个线程执行后是99999,不满足条件,后面几个线程计数增加到超过10w,而这时执行计数结果是10w那个线程满足条件(==10w),重置为0,那么就丢掉了超出10w的几个计数。计数就不准确了。当然条件是“>=”的时候,计数仍然不准确,而且会执行多次满足条件后的语句,打印多次日志,这显然不是我们想要的结果。有什么办法可以实现准确计数呢?AtomicInteger提供了一个updateAndGet方法,参数是实现IntUnaryOperator的类。看下它的实现:

    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

    updateFunction.applyAsInt(prev)这个方法返回我们希望重置的值。这样就简单了,我们只需要将超出部分的值,从applyAsInt方法返回就行了。具体的实现代码:

    private AtomicInteger counter = new AtomicInteger(0);//初始计数为0
    private long lastTime = 0;
    public void insert(){
      //insert
        if(counter.incrementAndGet() >= 100000) {
            counter.updateAndGet(new CounterVar());
            long currentTime = System.currentTimeMillis();
            log.info("\n\n=============== insert 10w data,time="+ currentTime+",used"+(currentTime-lastTime)+"'s ================\n\n");
            lastTime = currentTime;
      }
    }
    
    public class CounterVar implements IntUnaryOperator{
        @Override
        public int applyAsInt(int value) {
            if(value >= 100000) {
                return value-100000;
            }
            return value;
        }
    
    }
  • 相关阅读:
    【转载】Linux 内核启动时间分析
    hackbench
    c用户组函数
    c环境变量操作函数
    c网络接口套接字函数
    c信号处理函数
    c进程操作函数
    c文件内容操作函数
    c文件操作
    c数据结构和算法
  • 原文地址:https://www.cnblogs.com/johnvwan/p/15597733.html
Copyright © 2011-2022 走看看