使用波动性多头有什么意义吗?

2022-08-31 16:48:00

我偶尔会使用实例变量,如果我有两个线程从中读取/写入它,并且不希望取出锁的开销(或潜在的死锁风险);例如,一个计时器线程定期更新某个类上公开为 getter 的 int ID:volatile

public class MyClass {
  private volatile int id;

  public MyClass() {
    ScheduledExecutorService execService = Executors.newScheduledThreadPool(1);
    execService.scheduleAtFixedRate(new Runnable() {
      public void run() {
        ++id;
      }
    }, 0L, 30L, TimeUnit.SECONDS);
  }

  public int getId() {
    return id;
  }
}

我的问题是:鉴于JLS只保证32位读取将是原子的,那么使用易失性长线有什么意义吗?(即 64 位)。

注意:请不要回复说使用over是预优化的情况;我很清楚如何/何时使用,但在某些情况下是可取的。例如,在定义用于单线程应用程序的Spring Bean时,我倾向于使用实例变量,因为不能保证Spring上下文会在主线程中初始化每个Bean的属性。volatilesynchronizedsynchronizedvolatilevolatile


答案 1

不确定我是否正确地理解了您的问题,但是JLS 8.3.1.4.易失性字段指出:

字段可以声明为易失性字段,在这种情况下,Java 内存模型可确保所有线程都看到变量的一致值 (§17.4)。

也许更重要的是,JLS 17.7 双倍和长非原子处理

17.7 双重和长[...]

出于 Java 编程语言内存模型的目的,对非易失性长整型或双精度值的单次写入被视为两次单独的写入:每 32 位半部分一次写入。这可能会导致线程从一次写入中看到 64 位值的前 32 位,从另一次写入中看到第二个 32 位。易失性长整型和双精度值的写入和读取始终是原子的。对引用的写入和读取始终是原子的,无论它们是作为 32 位值还是 64 位值实现的。

也就是说,“整个”变量受可变修饰符的保护,而不仅仅是两个部分。这诱使我声称,对 s 使用易失性比对 s 使用易失性更重要,因为对于非易失性长整型/双精度,甚至连读取都不是原子的。longint


答案 2

这可以通过示例来证明

  • 不断切换两个字段,一个标记为易失性,另一个未在所有位之间设置并且所有位清除
  • 读取另一个线程上的字段值
  • 看到 foo 字段(不受易失性保护)可以在不一致的状态下读取,这永远不会发生在受易失性保护的条形场上

法典

public class VolatileTest {
    private long foo;
    private volatile long bar;
    private static final long A = 0xffffffffffffffffl;
    private static final long B = 0;
    private int clock;
    public VolatileTest() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    foo = clock % 2 == 0 ? A : B;
                    bar = clock % 2 == 0 ? A : B;
                    clock++;
                }
            }

        }).start();
        while (true) {
            long fooRead = foo;
            if (fooRead != A && fooRead != B) {
                System.err.println("foo incomplete write " + Long.toHexString(fooRead));
            }
            long barRead = bar;
            if (barRead != A && barRead != B) {
                System.err.println("bar incomplete write " + Long.toHexString(barRead));
            }
        }
    }

    public static void main(String[] args) {
        new VolatileTest();
    }
}

输出

foo incomplete write ffffffff00000000
foo incomplete write ffffffff00000000
foo incomplete write ffffffff
foo incomplete write ffffffff00000000

请注意,这只发生在32位VM上运行时,在64位VM上,我无法在几分钟内收到一个错误。


推荐