10

在一篇包含一些并发技巧的好文章中,一个示例被优化为以下几行:

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

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

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

4

3 回答 3

14

你是对的。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 类将在内部处理所有同步逻辑,因此调用者不必担心它。

于 2010-06-23T16:11:12.987 回答
1

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

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

double 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

    私有的 volatile boolean someAccountflag;

    public void getBalance() { Account acct; while (!someAccountflag) { acct = verify(name, password); } }

请注意,您不能将 someAccountflag 声明为已同步,因为您无法使用已同步的原语进行同步,同步仅适用于对象变量,其中原语或对象变量可能被声明为 volatile

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

于 2010-08-19T00:44:07.343 回答
1

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

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

但是对于上述代码,volatile如果多个线程修改平衡,则不保证内存一致性。 带有类型的AtomicReference 可以满足Double您的目的。

相关的 SE 问题:

Java 中 volatile 和 synchronized 的区别

于 2016-11-06T16:19:31.477 回答