长整型和双精度浮点数的赋值不是原子性操作 - 这有什么影响?

5
我们知道在Java中,直到声明为volatile之前,long和double类型的赋值都不是原子性操作。我的问题是,在我们的编程实践中这究竟有多重要。 例如,如果您看下面的类,它们的对象被多个线程共享。
/**
*  The below class is not thread safe. the assignments to int values would be 
*  atomic but at the same time it not guaranteed that changes would be visible to 
*  other threads.
**/
public final class SharedInt {

   private int value;

   public void setValue(int value) {
      this.value = value;
   }

   public int getValue() {
      return this.value;
   }

}

现在考虑另一个SharedLong。
/**
* The below class is not thread safe because here the assignments to  long 
*  are not atomic as well as changes are not
*  guaranteed to be visible to other threads.
*/
public final class SharedLong {

    private long value;

    public void setValue(long  value) {
      this.value = value;
    }

    public long getValue() {
       return this.values;
    }
}

现在我们可以看到上述两个版本都不是线程安全的。对于 int,因为线程可能会看到整数的旧值。而在 long 的情况下,它们可以看到 long 变量的损坏以及旧值。
在这两种情况下,如果一个实例不被多个线程共享,则类是安全的
要使上述类线程安全,我们需要声明 int 和 long 都是 volatile 或使方法同步
这让我想知道:在我们正常编程过程中,如果对longdouble赋值不是原子操作,那么它真的很重要吗? 因为这两个变量都需要声明为volatile或用于多线程访问的 synchronized,所以我的问题是:在哪些情况下,long赋值不是原子操作可能会有影响?

我已经回答了你的问题,但也许我误解了你的意思。你似乎已经理解了不声明变量为volatile的含义。还有什么需要补充的吗? - William Morrison
谢谢@WilliamMorrison,我已经编辑了我的问题,明确指出了我的意图,谢谢。 - veritas
好的,我现在已经为您提供了几种情况。 - William Morrison
据我所知,在Java 5.0之后这不是一个问题。 - Peter Lawrey
3个回答

11

我之前做了一个很酷的小例子。

public class UnatomicLong implements Runnable {
    private static long test = 0;

    private final long val;

    public UnatomicLong(long val) {
        this.val = val;
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            test = val;
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new UnatomicLong(-1));
        Thread t2 = new Thread(new UnatomicLong(0));

        System.out.println(Long.toBinaryString(-1));
        System.out.println(pad(Long.toBinaryString(0), 64));

        t1.start();
        t2.start();

        long val;
        while ((val = test) == -1 || val == 0) {
        }

        System.out.println(pad(Long.toBinaryString(val), 64));
        System.out.println(val);

        t1.interrupt();
        t2.interrupt();
    }

    // prepend 0s to the string to make it the target length
    private static String pad(String s, int targetLength) {
        int n = targetLength - s.length();
        for (int x = 0; x < n; x++) {
            s = "0" + s;
        }
        return s;
    }
}

其中一个线程不断地尝试将0分配给test,而另一个线程则尝试将-1分配给test。最终,您将得到一个数字,它要么是0b1111111111111111111111111111111100000000000000000000000000000000,要么是0b0000000000000000000000000000000011111111111111111111111111111111。(假设您不在64位JVM上。大多数64位JVM实际上会对longdouble进行原子赋值。)


实际上,这种特定的行为最近并没有成为问题,除非你仍然因某些原因在使用32位JVM。 - Kayaman
很棒的例子。感谢关于64位与32位JVM的注释。 - fracca

3

如果使用 int 进行不当编程可能会导致观察到过期值,而使用 long 进行不当编程可能会导致观察到实际上从未存在的值。

这在只需要最终正确而非时间点正确的系统中理论上可能很重要,因此为了提高性能跳过同步可能有所帮助。但是跳过易失性字段声明看起来似乎很愚蠢。


同意。我会将其设为volatile并继续。性能损失可能非常小,特别是考虑到避免的潜在错误。 - William Morrison
我同意,在某些情况下,看到一个过期的值可能不是问题,但是你不能在没有同步的情况下使用 long。 - veritas

2
如果同时访问SharedInt或SharedLong,会有所不同。正如您所说,一个线程可能会读取一个过时的int或过时或损坏的long。
如果这个值被用来引用数组,这可能很重要。
或者在GUI中显示。
那么在写入一些值并发送错误数据的情况下呢?现在客户端感到困惑或崩溃。
可能会将不正确的值存储到数据库中。
重复计算可能会被破坏...
正如您在评论中请求的那样,对于长整型:
长整型值经常用于时间计算。这可能会使您在等待一段时间执行某些操作(例如网络应用程序中的心跳)的循环中出现偏差。
你可以报告给客户端,与你同步时钟的时间是80年或1000年前。
Longs和ints通常用于位压缩字段以指示许多不同的事物。你的标志将完全被破坏。
长整型经常用作唯一的ID。这可能会破坏您正在创建的哈希表。
显然,很多坏事情可能会发生。如果这个值需要是线程安全的,并且您希望您的软件非常可靠,请声明这些变量易失性,使用原子变量或同步访问和设置方法。

无论如何,我必须使用int。那么long类型的赋值不是原子操作会有什么影响吗? - veritas
长赋值不是原子性的将会有相同的后果。 - William Morrison

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接