【问题标题】:Is the += operator thread-safe in Java?+= 运算符在 Java 中是线程安全的吗?
【发布时间】:2015-12-17 01:32:20
【问题描述】:

我找到了以下 Java 代码。

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

其中resultpartsdouble[]

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

【问题讨论】:

  • 操作不是原子,所以可能需要外部同步。
  • 您对所谓的基本操作也有些困惑(我假设您的意思是读取和写入)。确实,原始类型(例如 int)的读取是原子的,但这不会使其成为线程安全的。线程安全也涉及可见性。因此,即使线程 A 原子地将整数的值设置为 42,也不能保证线程 B 在之后执行原子读取时可以看到该值。
  • @JanusVarmarken:回复:“原始类型(比如 int)的读取确实是原子的”:是的,除了 long 和 double;见docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7
  • synchronized 放在循环周围可能会有所帮助 - 现在,您正在进入和离开循环的每次迭代的关键部分,这可能会主导运行时完全为了这段代码。
  • @assylias:这不是原子性的充分理由,实际上至少有两次读取,甚至可能更多(读取地址、读取偏移量、加载值,以及两次)。即使对于整个表达式或完整的循环,语言标准仍然可以要求原子性。

标签: java multithreading


【解决方案1】:

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

【讨论】:

【解决方案2】:

没有。 += 操作不是线程安全的。对于涉及分配给共享字段或数组元素的任何表达式,它需要锁定和/或适当的“之前发生”关系链,才能实现线程安全。

(使用声明为volatile 的字段,存在“先发生”关系......但仅在读取和写入操作上。+= 操作由读取和写入组成。这些是单独的原子,但序列不是。大多数使用= 的赋值表达式都涉及一次或多次读取(在右侧)和一次写入。sequence 也不是原子的。)

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

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

其实这是一个不正确的前提:

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

double 类型还有一个问题。 JLS (17.7) 这么说:

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

“volatile long 和 double 值的写入和读取始终是原子的。”


在评论中,您问:

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

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

如果您有int[]long[],您可以将它们替换为AtomicIntegerArrayAtomicLongArray,并利用这些类的无锁更新。但是没有AtomicDoubleArray 类,甚至没有AtomicDouble 类。

更新 - 有人指出 Guava 提供了一个 AtomicDoubleArray 类,因此 是一种选择。实际上是一个很好的选择。)

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

【讨论】:

  • Guava 有一个 AtomicDoubleArray 类,可用于解决 JDK 中缺少等效项的问题。此外,Java 8 引入了DoubleAdder 类,它基本上是一个原子double
  • @StephenC 我在多线程程序中对易失性数据成员使用了 +=。这会引起问题吗?
  • @Arya 很确定是的。 “volatile”告诉 JVM 从远程内存读取/写入最新版本的值,而不是使用本地缓存的版本。请参阅:A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable 只要各个线程读取单个其他线程写入的值,就可以了。一旦他们以交错方式读取/修改/写入值,所有的赌注仍然没有。保持安全,防御性编程!
  • @Arya - 是的。非常微妙的......就像偶尔丢失的更新。
  • 有了 Double.toLongbits 及其补充,您可以使用 AtomicLongArray(我认为番石榴在幕后使用)
【解决方案3】:

正如已经解释过的,这段代码不是线程安全的。在 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();

【讨论】:

    【解决方案4】:

    尽管在java中没有AtomicDoubleAtomicDoubleArray,你可以很容易地基于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.doubleToLongBitsDouble.longBitsToDoubleDoubles 存储为LongsAtomicLongArray 中。它们都具有相同的位大小,因此不会丢失精度(-NaN 除外,但我认为这并不重要)。

    在 Java 8 中,add 的实现可以更加简单,因为您可以使用在 java 1.8 中添加的 AtomicLongArrayaccumulateAndGet 方法。

    更新:看来我几乎重新实现了番石榴的AtomicDoubleArray

    【讨论】:

      猜你喜欢
      • 2010-12-15
      • 2011-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-20
      • 2012-10-03
      • 1970-01-01
      • 2016-08-24
      相关资源
      最近更新 更多