我发现了以下的Java代码。
for (int type = 0; type < typeCount; type++)
synchronized(result) {
result[type] += parts[type];
}
}
其中result
和parts
都是double[]
类型。
我知道基本类型的操作是线程安全的,但我不确定+=
是否也是。如果上述的synchronized
是必要的,那么可能有更好的类来处理这样的操作吗?
我发现了以下的Java代码。
for (int type = 0; type < typeCount; type++)
synchronized(result) {
result[type] += parts[type];
}
}
其中result
和parts
都是double[]
类型。
我知道基本类型的操作是线程安全的,但我不确定+=
是否也是。如果上述的synchronized
是必要的,那么可能有更好的类来处理这样的操作吗?
不行。 +=
操作不是线程安全的。 为了使涉及对共享字段或数组元素赋值的任何表达式都是线程安全的,需要锁定和/或适当的“happens-before”关系链。
(使用 volatile
声明的字段存在“happens-before”关系......但仅在读写操作时。 +=
操作由读取和写入组成。这些操作是单独原子的,但顺序却不是。大多数使用 =
的赋值表达式都涉及一个或多个读取(在右侧)和一个写入。连续的赋值表达式也不是原子的。)
有关完整说明,请阅读 JLS 17.4 或者 Brian Goetz 等人编写的《Java并发编程实战》相关章节。
据我所知,基本类型的基本操作是线程安全的.....
实际上,这是错误的前提:
对于 double
类型,还存在其他问题。 JLS (17.7) 说明了这一点:
""对于 Java 编程语言内存模型,对非易失性 long 或 double 值的单个写入被视为两个单独的写入:一个写入每个 32 位半部分。这可能导致线程从一个写入中看到 64 位值的第一个 32 位,并从另一个写入中看到第二个 32 位。"
对于volatile类型的long和double变量,它们的读写操作总是原子性的。
"在这种情况下(当您正在更新double[]
时),没有避免同步锁或原始互斥锁的替代方法。
如果您有一个int[]
或long[]
,您可以将它们替换为AtomicIntegerArray
或AtomicLongArray
,并利用这些类的无锁更新。 但是没有AtomicDoubleArray
类,甚至没有AtomicDouble
类。
(更新 - 有人指出Guava提供了一个AtomicDoubleArray
类,因此那就是一个选择。实际上是一个不错的选择。)
避免“全局锁”和大量争用问题的一种方法可能是将数组分成虚拟区域,每个区域都有自己的锁。这样,一个线程只需要阻塞另一个线程,如果它们正在使用数组的相同区域。(单个写入者/多个读取者锁也可以帮助解决问题...如果绝大部分访问都是读取操作。)
"AtomicDoubleArray
类,可以用来解决JDK中缺少相应类的问题。此外,Java 8还引入了DoubleAdder
类,它基本上是一个原子double
。 - Mick MnemonicAtomicDouble
或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));
}
}
Double.doubleToLongBits
和Double.longBitsToDouble
将Doubles
存储为AtomicLongArray
中的Longs
。它们在位数上具有相同的大小,因此不会丢失精度(除了- NaN,但我认为这不重要)。add
可以更加简单,因为您可以使用AtomicLongArray
的accumulateAndGet
方法,该方法已添加到java 1.8中。即使是普通的“double”数据类型在32位JVM中也不是线程安全的(因为它不是原子性的),因为在Java中它占用了8个字节(涉及2个32位操作)。
+=
操作了。看起来它应该是一个评论而不是一个回答。 - Dioxin正如已经解释过的那样,这段代码不是线程安全的。在Java-8中避免同步的一种可能的解决方案是使用新的DoubleAdder
类,该类能够以线程安全的方式维护双精度数的总和。
在并行化之前创建DoubleAdder
对象数组:
DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
.limit(typeCount).toArray(DoubleAdder[]::new);
for(int type = 0; type < typeCount; type++)
adders[type].add(parts[type]);
}
并行子任务完成后,最终获得结果:
double[] result = Arrays.stream(adders).mapToDouble(DoubleAdder::sum).toArray();
synchronized
放在循环周围可能会有很大帮助 - 目前,您每次迭代循环都会进入和离开临界区,这很可能会完全支配此代码片段的运行时。 - Luaan