使用 volatile long 有啥意义吗?

Posted

技术标签:

【中文标题】使用 volatile long 有啥意义吗?【英文标题】:Is there any point in using a volatile long?使用 volatile long 有什么意义吗? 【发布时间】:2011-03-03 13:34:03 【问题描述】:

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

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 的属性。

【问题讨论】:

注意,++ 不是原子操作。我建议改用java.util.concurrent.atomic.AtomicInteger 谢谢汤姆 - 这只是一个简单的例子。 顺便说一句,你不需要在 Spring 中使用volatile。 Spring 在初始化您的类时使用锁,并且发生前的关系将为您处理排序 【参考方案1】:

这可以通过例子来证明

不断切换两个字段,一个标记为易失性,一个不在所有位设置和所有位清除之间 在另一个线程上读取字段值 看到 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 位 VM 上运行时,在 64 位 VM 上,我在几分钟内无法得到一个错误。

【讨论】:

您还可以添加它出现在该输出中的原因,这是因为它在第一步中分两步写入第一个 32 位,在第二步中写入另一个 32 位。【参考方案2】:

“volatile”有多种用途:

保证原子写入双/长 保证当线程 A 看到线程 B 对 volatile 变量所做的更改时,线程 A 还可以看到线程 B 在更改为 volatile 变量之前所做的所有其他更改(考虑在设置细胞本身)。 基于只有一个线程可以更改变量的假设阻止编译器优化(想想紧密循环while (l != 0)

还有吗?

【讨论】:

【参考方案3】:

不确定我是否正确理解了您的问题,但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 修饰符的保护,而不仅仅是两个部分。这诱使我声称对longs 使用volatile 比对ints 使用更重要,因为甚至读取对于非不稳定的多头/双打。

【讨论】:

float value 呢,也是 16 字节? 我无法想象是什么导致了线程从一次写入中看到 64 位值的前 32 位,而从另一次写入中看到后 32 位的情况。你能告诉我吗? 一个线程将一个 64 位的值写入一个变量。由于这不是所有硬件上的原子操作,因此实际上可以分为两个写操作。另一个线程可能会出现并尝试读取这两个写操作之间的值。然后它将看到一个基于旧值的 32 位和新值的 32 位的值。请参阅下面亚当的回答。

以上是关于使用 volatile long 有啥意义吗?的主要内容,如果未能解决你的问题,请参考以下文章

javabean与vo有啥区别??

ANSI C 标准定义的基本数据类型取值范围数字有啥特殊意义

如果平台上“long”和“int”的大小相同 - “long”和“int”有啥不同吗?

mapDispatchToProps:有啥意义吗?

使用嵌套迭代器有啥意义吗?

使用非固定整数(int、long)而不是固定大小的整数(int64_t、int32_t)有啥好处吗?