AtomicInteger底层实现原理是什么?

典型回答

AtomicInteger是对int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS(compare-and-swap)技术。

所谓CAS,表现为一组指令,当利用CAS执行试图进行一些更新操作时。会首先比较当前数值,如果数值未变,代表没有其它线程进行并发修改,则成功更新。如果数值改变,则可能出现不同的选择,要么进行重试,要么就返回是否成功。也就是所谓的“乐观锁”。

从AtomicInteger的内部属性可以看出,它依赖于Unsafe提供的一些底层能力,进行底层操作;以volatile的value字段,记录数值,以保证可见性。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;

具体的原子操作细节,可以参考任意一个原子更新方法,比如下面的getAndIncrement。Unsafe会利用value字段的内存地址偏移,直接完成操作。

public final int getAndIncrement() {
  return U.getAndAddInt(this, VALUE, 1);
}

因为getAndIncrement需要返回数值,所以需要添加失败重试逻辑。

public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
    v = getIntVolatile(o, offset);
  } while (!weakCompareAndSetInt(o, offset, v, v + delta));
  return v;
}

而类似compareAndSet这种返回boolean类型的函数,因为其返回值表现的就是是否成功与否,所以不需要重试。

public final boolean compareAndSet(int expectedValue, int newValue)

CAS是Java并发中所谓lock-free机制的基础。

知识扩展

接下来我们通过一个例子看看如何利用Java标准库使用CAS。设想这样一个场景:在数据库产品中,为保证索引的一致性,一个常见的选择是,保证只有一个线程能够排他性地修改一个索引分区。如何在数据库抽象层面实现呢?

可以考虑为索引分区对象添加一个逻辑上的锁。例如,以当前独占的线程ID作为锁的数值,然后通过原子操作设置lock数值,来实现加锁和释放锁,伪代码如下:

public class AtomicBTreePartition {
  private volatile long lock;
  public void acquireLock() {}
  public void releaseLock() {}
}

那么在Java代码中,我们怎么实现锁操作呢?Unsafe似乎不是个好的选择,它设计之初仅仅打算提供给Java标准库自己使用,它的名字也暗示我们最好不要用它。目前Java提供了两种公共API,可以实现这种CAS操作。比如使用java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基于反射机制创建,我们需要保证类型和字段名正确。

private static final AtomicLongFieldUpdater<AtomicBTreePartition>
  LOCK_FIELD_UPDATER = AtomicLongFieldUpdater.newUpdater(
    AtomicBTreePartition.class,
    "lock");

private void acquireLock() {
  long t = Thread.currentThread().getId();
  while (!LOCK_FIELD_UPDATER.compareAndSet(this, 0L, t)) {
    // 等待一会儿,数据库操作可能比较慢。
    ...
  }
}

java.util.current.atomic包提供了最常用的原子性数据类型,甚至是引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。

如果是Java 9以后,我们完全可以采用另外一种方式实现,也就是Variable Handler API,提供了各种粒度的原子或有序性的操作。将前面的代码修改如下:

private static final VarHandle HANDLER = MethodHandles
  .lookup()
  .findStaticVarHandle(AtomicBTreePartition.class, "lock");

private void acquireLock() {
  long t = Thread.currentThread().getId();
  while (!HANDLE.compareAndSet(this, 0L, t)) {
    // 等待一会儿,数据库操作可能比较慢。
    ...
  }
}

过程非常直观,首先,获取相应的变量句柄,然后直接调用其提供的CAS方法。

一般来说,我们进行的类似CAS操作,可以并且推荐使用Variable Handler API去实现,其提供了精细粒度的公共底层API。这里强调“公共”,是因为其API不会内部API(例如Unsafe)那样,发生不可预测的修改。

CAS也并不是没有副作用。试想,其常用的失败重试机制,隐含着一个假设,即竞争情况是短暂的。大多数应用场景中,确实大部分重试只会发生一次就获得了成功。但是总是有意外情况,所以在有需要的时候,还是要考虑限制自旋的次数,以免过度消耗CPU。

另外一个就是著名的ABA问题,这是通常只在lock-free算法下暴露的问题。前面说过CAS是在更新时比较前值,如果对方只是恰好相同,例如期间发生了A->B->A的更新,仅仅判断数值是A,可能导致不合理的修改操作。针对这种情况,Java提供了AtomicStampedReference工具类。通过为引用建立类似版本号(stamp)的方式,来保证CAS的正确性。

前面介绍了CAS的场景与实现,幸运的是,大多数情况下,Java开发者并不需要直接利用CAS代码去实现线程安全容器等,更多是通过并发包等间接享受到ock-free机制在扩展性上的好处。

【完】

猜你喜欢

转载自blog.csdn.net/qweqwruio/article/details/81359887