AtomicIntegerArray 中的数据竞争

Data Races in an AtomicIntegerArray

在下面的代码中: 我在 2 个线程中每个更新 num[1]=0AtomicIntegerArray num 1000 次。

在主线程中 2 个线程的末尾;num[1] 的值不应该是 2000,因为 AtomicIntegerArray 中不应该有数据竞争。

但是我得到的随机值小于 2000。有人能告诉我为什么吗?

代码:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArr {

    private static AtomicIntegerArray num= new AtomicIntegerArray(2);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyRun1());
        Thread t2 = new Thread(new MyRun2());

        num.set(0, 10);
        num.set(1, 0);

        System.out.println("In Main num before:"+num.get(1));

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("In Main num after:"+num.get(1));
    }

    static class MyRun1 implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                num.set(1,num.get(1)+1);
            }

        }
    }

    static class MyRun2 implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                num.set(1,num.get(1)+1);
            }

        }

    }

}

编辑:添加 num.compareAndSet(1, num.get(1), num.get(1)+1); 而不是 num.set(1,num.get(1)+1); 也不起作用。

I get random values < 2000. Could someone tell me why?

这叫做the lost-update problem

因为,在下面的代码中:

num.set(1, num.get(1) + 1);

虽然涉及的每个单独操作都是原子的,但组合操作不是。来自两个线程的单个操作可以交错,导致来自一个线程的更新被另一个线程的陈旧值覆盖。

可以用compareAndSet解决这个问题,但是要检查操作是否成功,失败了再做。

int v;
do {
    v = num.get(1);
} while (!num.compareAndSet(1, v, v+1));

还有一种方法正是用于此目的:

num.accumulateAndGet(1, 1, (x, d)->x+d);

accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction)

Atomically updates the element at index i with the results of applying the given function to the current and given values, returning the updated value. The function should be side-effect-free, since it may be re-applied when attempted updates fail due to contention among threads. The function is applied with the current value at index i as its first argument, and the given update as the second argument.

这是一个典型的竞争条件。任何时候你有一个获取、一个操作和一个放置,你的代码都是活泼的。

考虑两个线程,它们都在大约 "same time." 时执行 num.set(1,num.get(1)+1) 首先,让我们分解一下表达式本身在做什么:

  • 它获取 num.get(1);我们称之为 x
  • 它加1;我们称之为 y
  • 它将那个和放在`num.set(1, y);

尽管表达式中的中间值只是堆栈上的值,而不是显式变量,但操作是相同的:get、add、put。

好的,回到我们的两个主题。如果这样操作会怎样?

inital state: n[1] = 5
Thread A      | Thread B
========================
x = n[1] = 5  |
              | x = n[1] = 5
              | y = 5 + 1 = 6
y = 5 + 1 = 6 | 
n[1] = 6      |
              | n[1] = 6

由于两个线程在任何一个线程放入其附加值之前都获取了值,因此它们都做同样的事情。你有 5 + 1 两次,结果是 6,而不是 7!

您想要的是 getAndIncrement(int idx),或者以原子方式执行获取、添加和放置的类似方法之一。

这些方法实际上都可以建立在您确定的 compareAndSet 方法之上。但是要做到这一点,您需要在一个循环中进行递增,尝试直到 compareAndSet returns 为真。此外,为了使其起作用,您已将初始 num.get(1) 值存储在局部变量中,而不是再次获取它。实际上,此循环表示 "keep trying the get-add-put logic until it works without anyone else having raced between the operations." 在我上面的示例中,线程 B 会注意到 compareAndSet(1, 5, 6) 失败(因为当时的实际值为 6,而不是预期的 5),因此会重试。这实际上是所有那些原子方法,如 getAndIncrement,所做的。