使用volatile是否有任何意义?

时间:2010-06-14 14:49:32

标签: java multithreading concurrency volatile

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

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位读取是原子的,永远使用volatile long有什么意义吗? (即64位)。

警告:请不要回复说使用volatile而不是synchronized是预先优化的情况;我很清楚如何/何时使用synchronized,但有些情况下volatile更可取。例如,在定义用于单线程应用程序的Spring bean时,我倾向于使用volatile实例变量,因为无法保证Spring上下文将初始化主线程中的每个bean的属性。

3 个答案:

答案 0 :(得分:121)

不确定我是否正确理解了您的问题,但JLS 8.3.1.4. volatile Fields说明了

  

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

,或许更重要的是,JLS 17.7 Non-atomic Treatment of double and long

  

17.7非原子性处理双重和长期
  [...]
  出于Java编程语言内存模型的目的,对非易失性long或double值的单次写入被视为两个单独的写入:每个32位半写一次。这可能导致线程从一次写入看到64位值的前32位,而从另一次写入看到第二次32位的情况。 volatile和long和double值的写入和读取始终是原子的。对引用的写入和读取始终是原子的,无论它们是实现为32位还是64位值。

也就是说,“整个”变量受volatile变体的保护,而不仅仅是两部分。这诱使我声称对long s使用volatile比<{1}}更强更重要,因为甚至没有读取非易失性多头/双打的原子。

答案 1 :(得分:9)

这可以通过示例

来证明
  • 不断切换两个字段,一个标记为volatile,另一个不在所有位设置和所有位清除
  • 之间
  • 读取另一个线程上的字段值
  • 看到foo字段(不受volatile保护)可以在不一致的状态下读取,这种情况永远不会发生在受volatile限制的bar字段中

代码

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位虚拟机上运行时,在64位虚拟机上我几分钟内无法收到任何错误。

答案 2 :(得分:3)

&#34;挥发性&#34;有多种用途:

  • 保证原子写入double / long
  • 保证当线程A看到线程B产生的volatile变量发生变化时,线程A也可以看到线程B在更改为volatile变量之前所做的所有其他更改(想想在设置之后设置数组中使用的单元格数量)细胞本身)。
  • 基于只有一个线程可以更改变量的假设来阻止编译器优化(认为紧密循环while (l != 0) {}

还有更多吗?