为什么在 AtomicInteger 和类似类的 getAndSet() 中有一个循环?

2022-09-02 04:23:10

在此代码中使用循环的目的

public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

答案 1

有一种思想流派认为,您应该尽可能节俭地使用锁。也就是说,如果可以避免使用锁,则永远不要使用锁,如果必须使用锁,请锁定最短的时间。这背后的原因源于有时将锁放在第一位的相当大的成本,以及一个线程等待而另一个线程在其所需资源上持有锁定的成本。

很长一段时间以来,CPU指令一直可用,称为比较和设置(或简称CAS),旨在帮助解决这个问题,基本上可以:

if (value == providedValue) {
  value = newValue;
  return true;
} else {
  return false;
}

这些指令可以在机器代码级别执行,并且比创建锁快得多。

想象一下,您希望使用这些指令之一以一种在高并行负载下始终正常工作的方式添加到数字中。显然,您可以将其编码为:1

int old = value;
if ( compareAndSet(old, old+1) ) {
  // It worked!
} else {
  // Some other thread incremented it before I got there.
}

但是,如果失败了,我们该怎么办呢?你猜对了 - 再试一次!CAS

boolean succeeded = false;
do {
  int old = value;
  if ( compareAndSet(old, old+1) ) {
    // It worked!
    succeeded = true;
  } else {
    // Some other thread incremented it before I got there. Just try again.
  }
} while (!succeeded);

在那里你可以看到你观察到的模式。

使用这个和类似的习语,可以使用根本没有锁来实现许多功能甚至一些相当复杂的数据结构(通常称为无锁)。例如,下面环形缓冲区的无锁实现。


答案 2

这些类表示原子数据类型。这意味着当两个或多个线程同时访问它们时,它们必须返回一致的结果。是通常直接在硬件中实现的操作,因此是按 .AtomicXXXcompareAndSetgetAndSetcompareAndSet

该方法的工作原理如下:首先,返回当前值。现在,另一个线程可能会同时更改该值,因此必须使用 检查它,情况并非如此。如果另一个线程更改了该值,则必须重复该过程,否则将返回错误的值。因此循环。compareAndSet


推荐