AtomicInteger的实现原理.md

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014453515/article/details/84960900

1.基本

AtomicInteger是对Integer类型的一个包装,提供原子性的访问和更新操作。其原子性的操作是基于CAS实现的。

CAS的过程是这样,执行运算时,使用当前数据值作为判断条件,利用CAS指令试图进行更新。更新之前获取内存中的最新值,与传来的当前值作比较。如果数值没有变,则说明没有其他线程进行并发修改,更新操作成功。则否则要么进行重试,要么返回结果。

2.AtomaticInteger的应用场景

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

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

3.源码分析

基本属性

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;
   public 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语言实现的。这里就不做深究了。

4.CAS操作的副作用

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

5.AtomicInteger计数怎么保证重置后的数值准确性

假设我们有这样一个需求,统计每次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的类。看下它的实现:

扫描二维码关注公众号,回复: 4515200 查看本文章
 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;
        }

    }

猜你喜欢

转载自blog.csdn.net/u014453515/article/details/84960900