是+;=Java中的运算符线程安全?
我发现了以下Java代码是+;=Java中的运算符线程安全?,java,multithreading,Java,Multithreading,我发现了以下Java代码 for (int type = 0; type < typeCount; type++) synchronized(result) { result[type] += parts[type]; } } for(int type=0;type
for (int type = 0; type < typeCount; type++)
synchronized(result) {
result[type] += parts[type];
}
}
for(int type=0;type
其中结果
和部分
是双[]
我知道基本类型上的基本操作是线程安全的,但我不确定
+=
。如果上面的synchronized
是必需的,是否有更好的类来处理此类操作?在32位JVM中,即使是正常的“double”数据类型也不是线程安全的(因为它不是原子的),因为它在Java中需要8个字节(涉及2*32位操作)。否。+=
操作不是线程安全的。对于任何涉及分配到共享字段或数组元素的表达式,它需要锁定和/或适当的“发生在”关系链,以确保线程安全
(如果字段声明为volatile
,则存在“发生在”关系…但仅在读写操作上存在。+=
操作由读操作和写操作组成。+=
操作分别是原子的,但序列不是原子的。使用=
的大多数赋值表达式都涉及一次或多次读操作(在右边)和写入。该序列也不是原子序列。)
有关完整的故事,请阅读JLS…或Brian Goetz等人撰写的“Java并发运行”的相关章节
据我所知,基本类型的基本操作是线程安全的
事实上,这是一个不正确的前提:
- 考虑数组的情况
- 考虑到表达式通常由一系列操作组成,并且原子操作序列不能保证是原子的
double
类型还有一个附加问题。JLS()说明:
在Java编程语言内存模型中,对非易失性长值或双值的一次写入被视为两次单独的写入:每32位的一半写入一次。这可能导致线程从一次写入中看到64位值的前32位,从另一次写入中看到第二个32位
“可变长值和双值的写入和读取始终是原子的。”
在评论中,你问: 那么,我应该使用什么类型来避免全局同步,它会停止这个循环中的所有线程 在这种情况下(您正在更新一个
double[]
),除了使用锁或基本互斥体进行同步之外,别无选择
如果你有一个int[]
或long[]
你可以用AtomicIntegerArray
或AtomicLongArray
替换它们,并利用这些类的无锁更新。但是没有AtomicDoubleArray
类,甚至没有AtomicDouble
类
(UPDATE-有人指出Guava提供了一个AtomicDoubleArray
类,所以这是一个选项。实际上是一个不错的选项。)
避免“全局锁”和大规模争用问题的一种方法可能是将数组划分为概念区域,每个区域都有自己的锁。这样,一个线程只需要在使用数组的同一区域时阻塞另一个线程。(如果绝大多数访问都是读取的,那么单写器/多读器锁也会有帮助。)正如前面所解释的,此代码不是线程安全的。避免Java-8中同步的一个可能解决方案是使用新的
双加器
类,该类能够以线程安全的方式维护两位数之和
在并行化之前创建双加法器对象数组:
DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
.limit(typeCount).toArray(DoubleAdder[]::new);
然后在并行线程中累积总和,如下所示:
for(int type = 0; type < typeCount; type++)
adders[type].add(parts[type]);
}
尽管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));
}
}
如您所见,我使用Double.Double-tolongbits
和Double.longBitsToDouble
在AtomicLongArray
中存储Double
作为Longs
。它们的位大小相同,因此精度不会丢失(除了-NaN,但我认为这并不重要)
在Java 8中,add
的实现更容易,因为您可以使用Java 1.8中添加的AtomicLongArray
的AccumerateAndget
方法
Upd:看来我实际上重新实现了guava的操作。该操作不是原子操作,这就是为什么可能需要外部同步的原因。您还对您所指的基本操作(我假设您指的是读和写)有点困惑。原语类型(例如int)的读取确实是原子的,但这并不能使其成为线程安全的。线程安全还涉及可见性。因此,即使线程a以原子方式将整数的值设置为42,也不能保证线程B在之后执行原子读取时会看到该值。@JanusVarmarken:Re:“原语类型(例如int)的读取确实是原子的”:是的,除了long和double;请参阅。将synchronized
放在循环中可能会有很大帮助-现在,循环的每个迭代都会进入和离开关键部分,这可能会完全支配这段代码的运行时。@assylias:这不是一个充分的理由对于原子性,实际上至少有两次读取,甚至可能更多(读取地址、读取偏移量、加载值,以及两次)。语言标准仍然可以强制要求原子性,即使对于完整表达式或完整循环也是如此。这是我第一次听说这一点。请向我解释为什么双数据类型中的线程安全依赖于JVM或OSE。您可以查看以下链接:@sharonbn@VA31此答案没有提到其他原语,更不用说
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));
}
}