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

在一篇包含一些并发技巧的文章中 ,一个示例针对以下几行进行了优化:

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

如果我理解正确,同步的关键是确保此线程读取的acct.balance的值是最新的,并且对acct.balance中对象的字段的任何挂起写入也写入主存储器。

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

你是对的。 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而不是synchronized”,答案很可能是“是”,但是确定它的时间/精力以及弄错它的危险并不值得利益(次要表现)。

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

声明账户不稳定会受到以下问题和限制

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

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

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

  2. 如果你需要同步来协调来自不同线程的变量的变化, volatile不能保证你的primefaces访问 ,因为访问volatile变量永远不会持有锁,它不适合我们想要read-update-write作为一个primefaces操作。 除非您确定acct = verify(名称,密码); 是单primefaces操作,你不能保证例外结果

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

  4. 相反,你可以像这里一样将boolean变量声明为volatile

    private volatile boolean someAccountflag;

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

请注意,您不能将someAccountflag声明为synchronized,因为您无法在带有synchronized的基元上进行同步, synchronized仅适用于对象变量,其中原始或对象变量可能被声明为volatile

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

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

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

但是对于上面的代码,如果多个threds修改平衡, volatile就不能保证内存的一致性。 具有Double类型的AtomicReference可满足您的需求。

相关SE问题:

Java中volatile和synchronized之间的区别