字段读取和volatile的同步之间的区别

时间:2010-06-23 15:45:15

标签: java concurrency synchronization volatile synchronized

在一个不错的article with some concurrency tips中,一个示例针对以下几行进行了优化:

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

如果我理解正确的话,同步的目的是确保此线程读取的acct.balance的值是最新的,并且还会写入对acct.balance中对象字段的任何挂起写入到主记忆。

这个例子让我想一想:将acct.balance(即类帐户的字段余额)声明为volatile会不会更有效率?它应该更高效,除了访问acct.balance之外的所有synchronize,并且不会锁定整个acct对象。我错过了什么吗?

3 个答案:

答案 0 :(得分:13)

你是对的。 volatile提供可见性保证。 synchronized提供可见性保证和受保护代码段的序列化。对于非常简单的情况,volatile就足够了,但是使用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而不是synchronized”,答案很可能是“肯定”,但是确定时间/努力以及弄错的危险是不值得受益(次要表现)。

另外,编写良好的Account类将在内部处理所有同步逻辑,因此调用者不必担心它。

答案 1 :(得分:1)

声明帐户易变,会受到以下问题和限制

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

double getBalance(){    volatile帐户acct = verify(名称,密码); //不正确..     }

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

  2. 如果需要同步以协调对来自不同线程的变量的更改, volatile不保证您具有原子访问权限,因为访问volatile变量永远不会持有锁,因此它不适合我们希望将read-update-write作为原子操作的情况。除非您确定acct = verify(名称,密码);是单原子操作,您不能保证例外结果

  3. 如果变量acct是一个对象引用,则可能是null。尝试在null对象上进行同步将使用synchronized抛出NullPointerException 。 (因为你实际上是在引用上同步,而不是实际对象) 挥发性不抱怨的地方

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

    private volatile boolean someAccountflag;

    public void getBalance(){ 帐户帐户; while(!someAccountflag){  acct = verify(名称,密码); } }

  5. 注意你不能将someAccountflag声明为synchronized,如 你不能在同步的基元上同步,同步仅适用于对象变量,其中原始或对象变量可以声明为volatile

    6. 类最终静态字段不需要是易变的,JVM负责此问题。所以如果someAccountflag是最终静态的,那么它甚至不需要声明为volatile 或者您可以使用延迟单例初始化将Account作为单例对象 并声明如下: private final static AccountSingleton acc_singleton = new AccountSingleton();

答案 2 :(得分:1)

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

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

但是对于上面的代码,volatile如果多个thred修改平衡,则不保证内存一致性。具有Double类型的AtomicReference符合您的目的。

相关的SE问题:

Difference between volatile and synchronized in Java