在Java中+=操作符线程安全吗?

时间:2021-09-19 11:47:21

I found the following Java code.

我找到了下面的Java代码。

for (int type = 0; type < typeCount; type++)
    synchronized(result) {
        result[type] += parts[type];
    }
}

where result and parts are double[].

其中结果和部分是双重的[]。

I know basic operations on primitive types are thread-safe, but I am not sure about +=. If the above synchronized is necessary, is there maybe a better class to handle such operation?

我知道基本类型的基本操作是线程安全的,但是我不确定+=。如果需要以上同步,是否有更好的类来处理此类操作?

4 个解决方案

#1


66  

No. The += operation is not thread-safe. It requires locking and / or a proper chain of "happens-before" relationships for any expression involving assignment to a shared field or array element to be thread-safe.

不。+=操作不是线程安全的。对于任何涉及分配到共享字段或数组元素的表达式,都需要锁定和/或一个适当的“事件之前”关系链,以确保线程安全。

(With a field declared as volatile, the "happens-before" relationships exist ... but only on read and write operations. The += operation consists of a read and a write. These are individually atomic, but the sequence isn't. And most assignment expressions using = involve both one or more reads (on the right hand side) and a write. That sequence is not atomic either.)

(有一个字段被声明为易变的,“之前发生的”关系存在……)但仅限于读写操作。+=操作由读和写组成。它们是单独的原子,但序列不是。大多数使用=的赋值表达式都包含一个或多个读(在右边)和一个写。这个序列也不是原子序列)

For the complete story, read JLS 17.4 ... or the relevant chapter of "Java Concurrency in Action" by Brian Goetz et al.

完整的故事,阅读JLS 17.4…或者是Brian Goetz等人的“Java并发在行动”的相关章节。

As I know basic operations on primitive types are thread-safe ...

如我所知,对原始类型的基本操作是线程安全的……

Actually, that is an incorrect premise:

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

  • consider the case of arrays
  • 考虑数组的情况
  • consider that expressions are typically composed of a sequence of operations, and that a sequence of atomic operations is not guaranteed to be atomic.
  • 考虑到表达式通常由一系列操作组成,并且不保证原子操作序列是原子操作。

There is an additional issue for the double type. The JLS (17.7) says this:

双类型还有一个额外的问题。JLS(17.7)指出:

"For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write."

对于Java编程语言内存模型的目的,对非易失性长值或双值的一次写入被视为两个单独的写入:一个到每个32位半的写入。这可能导致这样一种情况:线程看到一次写入的前32位64位值,而另一次写入的前32位值。

"Writes and reads of volatile long and double values are always atomic."

长值和双值的写和读总是原子的。


In a comment, you asked:

在评论中,你问道:

So what type I should use to avoid global synchronization, which stops all threads inside this loop?

那么我应该使用什么类型来避免全局同步,它会停止循环中的所有线程?

In this case (where you are updating a double[], there is no alternative to synchronization with locks or primitive mutexes.

在这种情况下(您正在更新一个double[]),除了使用锁或原始互斥对象进行同步之外,没有其他方法。

If you had an int[] or a long[] you could replace them with AtomicIntegerArray or AtomicLongArray and make use of those classes' lock-free update. However there is no AtomicDoubleArray class, or even an AtomicDouble class.

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

(UPDATE - someone pointed out that Guava provides an AtomicDoubleArray class, so that would be an option. A good one actually.)

(更新-有人指出番石榴提供了一个AtomicDoubleArray类,所以这是一个选项。一个好一个。)

One way of avoiding a "global lock" and massive contention problems might be to divide the array into notional regions, each with its own lock. That way, one thread only needs to block another thread if they are using the same region of the array. (Single writer / multiple reader locks could help too ... if the vast majority of accesses are reads.)

避免“全局锁”和大量争用问题的一种方法可能是将数组划分为不同的区域,每个区域都有自己的锁。这样,一个线程只需要在使用数组的相同区域时阻塞另一个线程。(单作者/多个读者锁也可以帮助……)如果绝大多数访问都是读操作的话。

#2


7  

Despite of the fact that there is no AtomicDouble or AtomicDoubleArray in java, you can easily create your own based on AtomicLongArray.

尽管在java中没有AtomicDouble或AtomicDoubleArray,但是您可以轻松地基于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));
    }
}

As you can see, I use Double.doubleToLongBits and Double.longBitsToDouble to store Doubles as Longs in AtomicLongArray. They both have the same size in bits, so precision is not lost (except for -NaN, but I don't think it is important).

你可以看到,我用了Double。doubleToLongBits和双。在AtomicLongArray中可以存储两倍的长数据。它们的位元大小相同,因此精度不会丢失(除了-NaN之外,但我认为这不重要)。

In Java 8 the implementation of add can be even easier, as you can use accumulateAndGet method of AtomicLongArray that was added in java 1.8.

在Java 8中,添加的实现可能更简单,因为您可以使用在Java 1.8中添加的AtomicLongArray的累加和get方法。

Upd: It appears that I virtually re-implemented guava's AtomicDoubleArray.

Upd:看来我实际上重新实现了番石榴的原子双重射线。

#3


6  

Even the normal 'double' data type is not thread-safe (because it is not atomic) in 32-bit JVMs as it takes eight bytes in Java (which involves 2*32 bit operations).

即使是正常的“双”数据类型在32位jvm中也不是线程安全的(因为它不是原子的),因为它在Java中占用8个字节(涉及2*32位操作)。

#4


3  

As it's already explained, this code is not thread safe. One possible solution to avoid synchronization in Java-8 is to use new DoubleAdder class which is capable to maintain the sum of double numbers in thread-safe manner.

如前所述,这段代码不是线程安全的。在Java-8中避免同步的一个可能的解决方案是使用新的DoubleAdder类,它能够以线程安全的方式维护双号的和。

Create array of DoubleAdder objects before parallelizing:

在并行化之前创建一系列的DoubleAdder对象:

DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
                             .limit(typeCount).toArray(DoubleAdder[]::new);

Then accumulate the sum in parallel threads like this:

然后像这样在并行线程中累积总和:

for(int type = 0; type < typeCount; type++) 
    adders[type].add(parts[type]);
}

Finally get the result after parallel subtasks finished:

最后得到并行子任务完成后的结果:

double[] result = Arrays.stream(adders).mapToDouble(DoubleAdder::sum).toArray();

#1


66  

No. The += operation is not thread-safe. It requires locking and / or a proper chain of "happens-before" relationships for any expression involving assignment to a shared field or array element to be thread-safe.

不。+=操作不是线程安全的。对于任何涉及分配到共享字段或数组元素的表达式,都需要锁定和/或一个适当的“事件之前”关系链,以确保线程安全。

(With a field declared as volatile, the "happens-before" relationships exist ... but only on read and write operations. The += operation consists of a read and a write. These are individually atomic, but the sequence isn't. And most assignment expressions using = involve both one or more reads (on the right hand side) and a write. That sequence is not atomic either.)

(有一个字段被声明为易变的,“之前发生的”关系存在……)但仅限于读写操作。+=操作由读和写组成。它们是单独的原子,但序列不是。大多数使用=的赋值表达式都包含一个或多个读(在右边)和一个写。这个序列也不是原子序列)

For the complete story, read JLS 17.4 ... or the relevant chapter of "Java Concurrency in Action" by Brian Goetz et al.

完整的故事,阅读JLS 17.4…或者是Brian Goetz等人的“Java并发在行动”的相关章节。

As I know basic operations on primitive types are thread-safe ...

如我所知,对原始类型的基本操作是线程安全的……

Actually, that is an incorrect premise:

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

  • consider the case of arrays
  • 考虑数组的情况
  • consider that expressions are typically composed of a sequence of operations, and that a sequence of atomic operations is not guaranteed to be atomic.
  • 考虑到表达式通常由一系列操作组成,并且不保证原子操作序列是原子操作。

There is an additional issue for the double type. The JLS (17.7) says this:

双类型还有一个额外的问题。JLS(17.7)指出:

"For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write."

对于Java编程语言内存模型的目的,对非易失性长值或双值的一次写入被视为两个单独的写入:一个到每个32位半的写入。这可能导致这样一种情况:线程看到一次写入的前32位64位值,而另一次写入的前32位值。

"Writes and reads of volatile long and double values are always atomic."

长值和双值的写和读总是原子的。


In a comment, you asked:

在评论中,你问道:

So what type I should use to avoid global synchronization, which stops all threads inside this loop?

那么我应该使用什么类型来避免全局同步,它会停止循环中的所有线程?

In this case (where you are updating a double[], there is no alternative to synchronization with locks or primitive mutexes.

在这种情况下(您正在更新一个double[]),除了使用锁或原始互斥对象进行同步之外,没有其他方法。

If you had an int[] or a long[] you could replace them with AtomicIntegerArray or AtomicLongArray and make use of those classes' lock-free update. However there is no AtomicDoubleArray class, or even an AtomicDouble class.

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

(UPDATE - someone pointed out that Guava provides an AtomicDoubleArray class, so that would be an option. A good one actually.)

(更新-有人指出番石榴提供了一个AtomicDoubleArray类,所以这是一个选项。一个好一个。)

One way of avoiding a "global lock" and massive contention problems might be to divide the array into notional regions, each with its own lock. That way, one thread only needs to block another thread if they are using the same region of the array. (Single writer / multiple reader locks could help too ... if the vast majority of accesses are reads.)

避免“全局锁”和大量争用问题的一种方法可能是将数组划分为不同的区域,每个区域都有自己的锁。这样,一个线程只需要在使用数组的相同区域时阻塞另一个线程。(单作者/多个读者锁也可以帮助……)如果绝大多数访问都是读操作的话。

#2


7  

Despite of the fact that there is no AtomicDouble or AtomicDoubleArray in java, you can easily create your own based on AtomicLongArray.

尽管在java中没有AtomicDouble或AtomicDoubleArray,但是您可以轻松地基于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));
    }
}

As you can see, I use Double.doubleToLongBits and Double.longBitsToDouble to store Doubles as Longs in AtomicLongArray. They both have the same size in bits, so precision is not lost (except for -NaN, but I don't think it is important).

你可以看到,我用了Double。doubleToLongBits和双。在AtomicLongArray中可以存储两倍的长数据。它们的位元大小相同,因此精度不会丢失(除了-NaN之外,但我认为这不重要)。

In Java 8 the implementation of add can be even easier, as you can use accumulateAndGet method of AtomicLongArray that was added in java 1.8.

在Java 8中,添加的实现可能更简单,因为您可以使用在Java 1.8中添加的AtomicLongArray的累加和get方法。

Upd: It appears that I virtually re-implemented guava's AtomicDoubleArray.

Upd:看来我实际上重新实现了番石榴的原子双重射线。

#3


6  

Even the normal 'double' data type is not thread-safe (because it is not atomic) in 32-bit JVMs as it takes eight bytes in Java (which involves 2*32 bit operations).

即使是正常的“双”数据类型在32位jvm中也不是线程安全的(因为它不是原子的),因为它在Java中占用8个字节(涉及2*32位操作)。

#4


3  

As it's already explained, this code is not thread safe. One possible solution to avoid synchronization in Java-8 is to use new DoubleAdder class which is capable to maintain the sum of double numbers in thread-safe manner.

如前所述,这段代码不是线程安全的。在Java-8中避免同步的一个可能的解决方案是使用新的DoubleAdder类,它能够以线程安全的方式维护双号的和。

Create array of DoubleAdder objects before parallelizing:

在并行化之前创建一系列的DoubleAdder对象:

DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
                             .limit(typeCount).toArray(DoubleAdder[]::new);

Then accumulate the sum in parallel threads like this:

然后像这样在并行线程中累积总和:

for(int type = 0; type < typeCount; type++) 
    adders[type].add(parts[type]);
}

Finally get the result after parallel subtasks finished:

最后得到并行子任务完成后的结果:

double[] result = Arrays.stream(adders).mapToDouble(DoubleAdder::sum).toArray();