Java中的+ =运算符是否是线程安全的?

Pio*_*app 51 java multithreading

我找到了以下Java代码.

for (int type = 0; type < typeCount; type++)
    synchronized(result) {
        result[type] += parts[type];
    }
}
Run Code Online (Sandbox Code Playgroud)

其中,resultpartsdouble[].

我知道原始类型的基本操作是线程安全的,但我不确定+=.如果上述synchronized是必要的,是否有更好的类来处理这种操作?

Ste*_*n C 66

+=不可以.该操作不是线程安全的.对于涉及将共享字段或数组元素赋值为线程安全的任何表达式,它需要锁定和/或适当的"事先发生"关系链.

(将字段声明为volatile,"之前发生"关系存在...但仅限于读取和写入操作.+=操作包括读取和写入.这些是单独原子的,但序列不是.赋值表达式使用=涉及一个或多个读取(在右侧)和写入.该序列也不是原子的.)

有关完整的故事,请阅读JLS 17.4 ...或Brian Goetz等人的"Java Concurrency in Action"的相关章节.

据我所知,基本类型的基本操作是线程安全的...

实际上,这是一个不正确的前提:

  • 考虑数组的情况
  • 考虑到表达式通常由一系列操作组成,并且原子操作序列不能保证是原子的.

double类型还有一个问题.JLS(17.7)说:

"出于Java编程语言内存模型的目的,对非易失性long或double值的单个写入被视为两个单独的写入:每个32位半写一个.这可能导致线程看到的情况一次写入的64位值的前32位,另一次写入的第二位32位."

"写入和读取volatile的long和double值总是原子的."


在评论中,你问:

那么我应该使用哪种类型来避免全局同步,这会阻止此循环中的所有线程?

在这种情况下(您正在更新a double[],除了与锁或原始互斥锁同步之外别无选择.

如果您有int[]或者long[]您可以用AtomicIntegerArray或替换它们AtomicLongArray并使用这些类的无锁更新.但是没有AtomicDoubleArray课,甚至没有AtomicDouble课.

(更新 - 有人指出Guava提供了一个AtomicDoubleArray类,所以这是一个选项.实际上是一个好的.)

避免"全局锁定"和大规模争用问题的一种方法可能是将阵列划分为名义区域,每个区域都有自己的锁定.这样,一个线程只需要阻止另一个线程,如果它们使用相同的数组区域.(单个写入器/多个读取器锁也可以帮助...如果绝大多数访问都是读取.)

  • Guava有一个[`AtomicDoubleArray`](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/util/concurrent/AtomicDoubleArray.html)类,可以在这里用来解决缺少JDK中的等价物.另外,Java 8引入了[`DoubleAdder`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/DoubleAdder.html)类,这基本上是一个原子` double`. (6认同)
  • @Arya很确定是的."volatile"告诉JVM从远程内存读取/写入最新版本的值,而不是使用本地缓存版本.请参阅:[字段可能被声明为volatile,在这种情况下,Java内存模型可确保所有线程都看到变量的一致值](https://docs.oracle.com/javase/specs/jls/se8/html/ jls-8.html#jls-8.3.1.4)只要各个线程只是_read_由一个其他线程写的值,那就没问题了.一旦他们以交错的方式读取/修改/写入值,所有的赌注仍然是关闭的.安全,防守程序! (2认同)
  • @Arya - 是的.非常微妙的......就像偶尔丢失更新一样. (2认同)

Aiv*_*ean 7

尽管事实上没有AtomicDoubleAtomicDoubleArray在java中,您可以轻松地创建自己的基于AtomicLongArray.

static class AtomicDoubleArray {
    private final AtomicLongArray inner;

    public AtomicDoubleArray(int length) {
        inner = new AtomicLongArray(length);
    }

    public int length() {
        return inner.length();
    }

    public double get(int i) {
        return Double.longBitsToDouble(inner.get(i));
    }

    public void set(int i, double newValue) {
        inner.set(i, Double.doubleToLongBits(newValue));
    }

    public void add(int i, double delta) {
        long prevLong, nextLong;
        do {
            prevLong = inner.get(i);
            nextLong = Double.doubleToLongBits(Double.longBitsToDouble(prevLong) + delta);
        } while (!inner.compareAndSet(i, prevLong, nextLong));
    }
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我使用Double.doubleToLongBitsDouble.longBitsToDouble存储DoublesLongsAtomicLongArray.它们都具有相同的位大小,因此精度不会丢失(除了-NaN,但我认为它不重要).

在Java 8中,实现add可以更容易,因为您可以使用在Java 1.8中添加的accumulateAndGet方法AtomicLongArray.

更新:看来我实际上重新实现了番石榴的AtomicDoubleArray.


dev*_*per 6

即使是正常的"双"数据类型在32位JVM中也不是线程安全的(因为它不是原子的),因为它在Java中需要8个字节(涉及2*32位操作).

  • @VinceEmigh - 他是对的.JLS这样说.看我的回答. (4认同)