【问题标题】:Difference between synchronization of field reads and volatile字段读取同步和易失性之间的区别
【发布时间】:2011-03-07 09:39:08
【问题描述】:

在一个不错的article with some concurrency tips 中,一个示例被优化为以下几行:

double getBalance() {
    Account acct = verify(name, password);
    synchronized(acct) { return acct.balance; }
}

如果我理解正确,同步的重点是确保该线程读取的 acct.balance 的值是最新的,并且任何挂起的对 acct.balance 中对象字段的写入也被写入到主内存。

这个例子让我想到了一点:将acct.balance(即类Account的字段余额)声明为volatile不是更有效吗?它应该更有效,在访问 acct.balance 时保存所有 synchronize 并且不会锁定整个 acct 对象。我错过了什么吗?

【问题讨论】:

  • 你是对的,但这篇文章实际上是关于完全不同的东西 - 减少锁定范围。

标签: java concurrency synchronization volatile synchronized


【解决方案1】:

你是对的。 volatile 提供了可见性保证。 synchronized 提供可见性保证和受保护代码段的序列化。对于非常简单的情况 volatile 就足够了,但是使用 volatile 代替同步很容易遇到麻烦。

如果您假设 Account 可以调整其余额,那么 volatile 还不够好

public void add(double amount)
{
   balance = balance + amount;
}

如果余额不稳定且没有其他同步,我们就会遇到问题。如果两个线程尝试一起调用 add() ,则可能会发生“错过”更新

Thread1 - Calls add(100)
Thread2 - Calls add(200)
Thread1 - Read balance (0)
Thread2 - Read balance (0)
Thread1 - Compute new balance (0+100=100)
Thread2 - Compute new balance (0+200=200)
Thread1 - Write balance = 100
Thread2 - Write balance = 200 (WRONG!)

显然这是错误的,因为两个线程都读取当前值并独立更新,然后将其写回(读取、计算、写入)。 volatile 在这里没有帮助,因此您需要同步以确保一个线程在另一个线程开始之前完成整个更新。

我一般发现,如果在编写一些代码时我认为“我可以使用 volatile 而不是同步”,答案很可能是“是”,但是确定它的时间/努力以及出错的危险是不值得(表现不佳)。

顺便说一句,一个编写良好的 Account 类将在内部处理所有同步逻辑,因此调用者不必担心。

【讨论】:

    【解决方案2】:

    将帐户声明为 volatile 会受到以下问题和限制

    1.“由于其他线程看不到局部变量,将局部变量声明为 volatile 是徒劳的。”此外,如果您尝试在方法中声明 volatile 变量,在某些情况下会出现编译器错误。

    双 getBalance() { volatile Account acct = verify(name, password); //不正确.. }

    1. 将 Account 声明为 volatile 会警告编译器每次都重新获取它们,而不是将它们缓存在寄存器中。这也禁止某些优化,假设没有其他线程会意外更改值。

    2. 如果您需要同步以协调对来自不同线程的变量的更改, volatile 不保证你的原子访问,因为访问 volatile 变量永远不会持有锁,它不适合我们想将 read-update-write 作为原子操作的情况。除非您确定 acct = verify(name, password);是单原子操作,不能保证异常结果

    3. 如果变量 acct 是一个对象引用,那么它很可能是 null 。尝试对 null 对象进行同步将使用 synchronized 引发 NullPointerException。 (因为您正在有效地同步参考,而不是实际对象) volatile 不抱怨的地方

    4. 相反,您可以像这里一样将布尔变量声明为 volatile

      private volatile boolean someAccountflag;

      公共无效getBalance(){ 账户账户; 而(!someAccountflag){ acct = 验证(名称,密码); } }

    请注意,您不能将 someAccountflag 声明为同步,如 你不能用 synchronized 对原语进行同步,同步只适用于对象变量,其中原语或对象变量可能被声明为 volatile

    6.类 final 静态字段不需要是 volatile,JVM 会处理这个问题。因此,如果 someAccountflag 是最终静态的,则甚至不需要将其声明为 volatile 或者您可以使用惰性单例初始化将 Account 作为单例对象 并声明如下: private final static AccountSingleton acc_singleton = new AccountSingleton ();

    【讨论】:

    • 谢谢你的回答,但我其实建议不要将acct声明为volatile,而是acct.balance——即类Account的字段balance。这会修改您的一些 cmets。我试图在这方面稍微澄清一下这个问题。
    【解决方案3】:

    如果多个线程在修改和访问数据,synchronized保证多个线程之间的数据一致性。

    如果单线程修改数据,多线程尝试读取数据的最新值,使用volatile构造。

    但是对于上述代码,volatile 不保证多个线程修改平衡时的内存一致性。 AtomicReferenceDouble 类型服务于您的目的。

    相关的 SE 问题:

    Difference between volatile and synchronized in Java

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-14
      • 2012-04-02
      • 2018-01-18
      • 2021-04-18
      • 1970-01-01
      • 2014-04-27
      • 2021-10-16
      • 2019-09-28
      相关资源
      最近更新 更多