使用volatile是否有任何意义?

Ada*_*ski 53 java concurrency multithreading 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

我的问题:鉴于JLS只能保证32位读取将是原子有任何一点曾经使用挥发性长时间?(即64位).

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

aio*_*obe 123

不确定我是否正确理解了您的问题,但是JLS 8.3.1.4.volatile字段:

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

并且,或许更重要的是,JLS 17.7双原子和非原子的非原子化处理:

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

也就是说,"整个"变量由volatile修饰符保护,而不仅仅是两个部分.这诱使我声称使用volatile 比s 更重要,因为即使读取对于非易失性longs/double 也不是原子的.longint

  • @shaoyihe 你是从那里得到那16个字节的吗?Java 中没有具有该大小的原始类型。`float` 是一个 32 位(==4 字节)的数量,类似于 `int`。 (2认同)

Ada*_*dam 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

产量

foo incomplete write ffffffff00000000
foo incomplete write ffffffff00000000
foo incomplete write ffffffff
foo incomplete write ffffffff00000000
Run Code Online (Sandbox Code Playgroud)

请注意,这只适用于在32位虚拟机上运行时,在64位虚拟机上我几分钟内无法获得单个错误.

  • 您还可以添加为什么会出现该输出,因为它在第一步中分两步写入第一个 32 位,在第二步中写入另一个 32 位。 (2认同)

小智 6

“volatile”有多种用途:

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

还有更多吗?