如果我有一个线程之间共享的对象,在我看来,每个字段都应为final
或volatile
,其理由如下:
如果应更改该字段(指向另一个对象,更新原始值),则该字段应为volatile
,以便所有其他线程都对新值进行操作。仅访问这些字段的方法之间的同步是不够的,因为它们可能返回缓存的值。
如果该字段永远不会更改,请将其设置为final
。
但是,我对此一无所获,所以我想知道这种逻辑是有缺陷的还是太明显了?
编辑当然可以使用final AtomicReference
或类似的符号来代替volatile。
编辑,例如,参见Is getter method an alternative to volatile in Java?
编辑,以避免混淆:这个问题与缓存失效有关!如果两个线程对同一个对象进行操作,则可以缓存对象的字段(每个线程) ,如果它们没有声明为volatile。如何保证缓存正确无效?
最终编辑感谢@Peter Lawrey向我指出了JLS§17(Java内存模型)。据我所知,它指出同步在操作之间建立了事前发生的关系,因此,如果这些更新“发生在事前”,则线程可以看到另一个线程的更新。如果非易失性字段的getter和setter是synchronized
。
答案 0 :(得分:31)
虽然我觉得private final
应该是带有var
这样的关键字的字段和变量的默认设置,使其可变,但在不需要时使用volatile
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.因此,如果您需要保证没有过时的值,则应该保护该值,并保护该值周围的锁。
在共享对象上将锁和值设为私有,因此这里的操作是实现细节。
为避免死锁,此操作应为“原子”操作,以避免与其他任何锁发生交互。