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

}

相关