Java:将所有字段设置为final还是volatile?

时间:2018-12-12 08:13:00

标签: java multithreading volatile final

如果我有一个线程之间共享的对象,在我看来,每个字段都应为finalvolatile,其理由如下:

  • 如果应更改该字段(指向另一个对象,更新原始值),则该字段应为volatile,以便所有其他线程都对新值进行操作。仅访问这些字段的方法之间的同步是不够的,因为它们可能返回缓存的值。

  • 如果该字段永远不会更改,请将其设置为final

但是,我对此一无所获,所以我想知道这种逻辑是有缺陷的还是太明显了?

编辑当然可以使用final AtomicReference或类似的符号来代替volatile。

编辑,例如,参见Is getter method an alternative to volatile in Java?

编辑,以避免混淆:这个问题与缓存失效有关!如果两个线程对同一个对象进行操作,则可以缓存对象的字段(每个线程) ,如果它们没有声明为volatile。如何保证缓存正确无效?

最终编辑感谢@Peter Lawrey向我指出了JLS§17(Java内存模型)。据我所知,它指出同步在操作之间建立了事前发生的关系,因此,如果这些更新“发生在事前”,则线程可以看到另一个线程的更新。如果非易失性字段的getter和setter是synchronized

3 个答案:

答案 0 :(得分:31)

虽然我觉得private final应该是带有var这样的关键字的字段和变量的默认设置,使其可变,但在不需要时使用volatile

  • 慢得多,通常慢10倍左右。
  • 通常不会为您提供所需的线程安全性,但是可以通过减少出现这些错误的可能性来使发现此类错误更加困难。
  • final不同,它通过说不应该更改此内容(在不需要时使用volatile来提高清晰度),可能会引起读者的困惑,因为读者试图找出原因使其变得易变。
  

如果应该更改该字段(指向另一个对象,更新原始值),则该字段应该是易失性的,以便所有其他线程都对新值进行操作。

虽然这很适合阅读,但请考虑一下这种琐碎的情况。

volatile int x;

x++;

这不是线程安全的。与

相同
int x2 = x;
x2 = x2 + 1; // multiple threads could be executing on the same value at this point.
x = x2;

更糟糕的是,使用volatile会使这种错误更难发现。

yshavit指出,volatile很难更新多个字段,例如HashMap.put(a, b)更新多个参考。

  

仅访问这些字段的方法之间的同步是不够的,因为它们可能返回缓存的值。

同步化为您提供volatile以及更多的所有内存保证,这就是为什么它要慢得多的原因。

注意:仅synchronized-每种方法也不总是足够的。 StringBuffer同步了每种方法,但是在多线程上下文中,这种方法比没有用的方法更糟,因为它的使用很容易出错。

很容易假设实现线程安全就像撒上仙女般的尘土,增加一些魔术线程安全性,然后您的错误消失了。问题在于线程安全性更像是带有多个孔的存储桶。塞住最大的漏洞,这些错误似乎消失了,但是除非全部塞入,否则就没有线程安全性,但是很难找到。

就同步与易失性而言,这表明

  

其他机制,例如易失变量的读写,以及java.util.concurrent包中类的使用,提供了替代的同步方式。

https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

答案 1 :(得分:8)

使您无需更改final的字段是一个好主意,而与线程问题无关。它使类的实例更易于推理,因为您可以更轻松地知道其处于什么状态。

关于使其他字段为volatile

  

仅访问这些字段的方法之间的同步是不够的,因为它们可能返回缓存的值。

只有在访问同步块之外的值时,才会看到缓存的值。

所有访问都需要正确同步。保证一个同步块的结束发生在另一个同步块的开始之前(在同一监视器上同步时)。

至少在某些情况下,您仍然需要使用同步:

  • 如果必须先阅读然后自动更新一个或多个字段,则希望使用同步。
    • 您可能能够避免某些单个字段更新的同步,例如如果可以使用Atomic*类而不是“普通字段”;但即使是一次字段更新,您仍然可能需要独占访问权限(例如,将一个元素添加到列表中,同时删除另一个元素)。
  • 此外,对于非线程安全值(例如ArrayList或数组),volatile / final可能不足。

答案 2 :(得分:1)

如果对象在线程之间共享,则有两个明确的选择:

1。将该对象设为只读

因此,更新(或缓存)没有影响。

2。同步对象本身

缓存失效很难。 Very hard.因此,如果您需要保证没有过时的值,则应该保护该值,并保护该值周围的锁

在共享对象上将锁和值设为私有,因此这里的操作是实现细节。

为避免死锁,此操作应为“原子”操作,以避免与其他任何锁发生交互。