Java:使所有字段成为最终字段或可变字段?
Posted
技术标签:
【中文标题】Java:使所有字段成为最终字段或可变字段?【英文标题】:Java: Make all fields either final or volatile? 【发布时间】:2019-05-13 07:13:04 【问题描述】:如果我有一个在线程之间共享的对象,在我看来,每个字段都应该是final
或volatile
,原因如下:
如果该字段应该改变(指向另一个对象,更新原始值),那么该字段应该是volatile
,以便所有其他线程对新值进行操作。仅仅对访问所述字段的方法进行同步是不够的,因为它们可能会返回缓存值。
如果该字段永远不会更改,请设置为final
。
但是,我找不到任何关于此的信息,所以我想知道这个逻辑是否有缺陷或过于明显?
EDIT当然可以使用final AtomicReference
或类似名称来代替易失性。
EDIT 例如,参见Is getter method an alternative to volatile in Java?
EDIT 避免混淆:这个问题是关于缓存失效的! 如果两个线程对同一个对象进行操作,对象的字段可以被缓存(每个线程) ,如果它们没有被声明为 volatile。如何保证缓存正确失效?
最终编辑感谢@Peter Lawrey,他向我指出了 JLS §17(Java 内存模型)。据我所知,它声明同步在操作之间建立了先发生关系,因此如果这些更新“发生在之前”,则线程可以看到来自另一个线程的更新,例如如果非易失性字段的 getter 和 setter 是 synchronized
。
【问题讨论】:
Merely a synchronization on the methods which access said field is insufficient
- 不,同步比volatile
“更强”。这意味着如果另一个线程在其中,您甚至都不会进入该方法,因此您只能在另一个线程退出并完成更改值后进入。
问题是每个线程都可以有自己的缓存版本,所以线程1更新后线程2仍然可以看到旧版本。 tutorials.jenkov.com/java-concurrency/volatile.html
不,当正确使用private
字段时,所有访问都必须通过同步方法完成,最新值的可见性已经得到保证。正如@daniu 正确指出的那样,它甚至比声明变量volatile
更强大。正如您链接的文章所指出的那样,volatile 并不总是足够的。
“易失性”是关于“缓存失效”的概念是错误的推理;它预设了一个特定的内存模型实现,即内存是用复制真实内存的处理器缓存实现的,或者变量是用缓存值的寄存器实现的。 Java 提供给您的抽象内存模型中正确性的原因,而不是它的任何特定实现。
至此:volatile 不是你撒在程序上的魔法尘埃,线程问题就会消失。易失性读取和写入仍然可能相互重新排序,这仍然会产生意外的竞争条件。 不要让易失性字段给您对程序正确性的不劳而获的安全感。同样,您需要在语言的内存模型的上下文中推理程序的正确性。
【参考方案1】:
虽然我觉得 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)
更新多个引用。
仅仅对访问所述字段的方法进行同步是不够的,因为它们可能会返回缓存值。
synchronized 为您提供volatile
的所有内存保证以及更多,这就是它明显变慢的原因。
注意:仅仅synchronized
-ing 每种方法也并不总是足够的。 StringBuffer
已同步所有方法,但在多线程上下文中比无用更糟糕,因为它的使用可能容易出错。
很容易假设实现线程安全就像撒仙尘一样,添加一些神奇的线程安全,你的错误就会消失。问题是线程安全更像是一个有很多洞的桶。堵住最大的洞,bug 似乎就消失了,但除非你把它们都堵上,否则你没有线程安全,但它可能更难找到。
就同步与易失而言,这表明
其他机制,例如 volatile 变量的读取和写入以及使用 java.util.concurrent 包中的类,提供了替代的同步方式。
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
【讨论】:
你能解释一下为什么,因为 thread1 和 thread2 持有对同一个对象的引用,并且只能通过同步方法访问,线程 2 会看到线程 1 的更新保证?我指的是例如tutorials.jenkov.com/java-concurrency/volatile.html 这个和其他资源说(据我所知)这些字段仍然可以缓存在不同的缓存行中——也就是说,线程 2 只需要等待 getX() 进入,但它仍然会返回缓存的版本。有没有保证线程的缓存在获取到监视器等之后更新? @Moritz 同步将毫无意义,如果没有内存屏障来提供这些保证。当值缓存在同步块中时,它将在读取时返回最新值并确保写入的任何内容对所有线程都是可见的。 对我来说完全有道理,但是您是否偶然知道让我们在 JLS 中说明这一点的任何一点? 据我所知,从 JLS §17 可以看出,如果访问器方法是同步的,这会在访问之间引入发生前的关系,因此第二个操作将看到更新。对吗? 这是一个很好的答案,但我建议快速注意,类经常使用 multiple 字段中的状态,而 volatile 无法解决。这类似于您的x++
案例,但更不微妙,更难解决(即,您不会得到“哦,这很容易,只需使用 AtomicInteger”)。【参考方案2】:
创建不需要更改的字段final
是一个好主意,与线程问题无关。它使类的实例更易于推理,因为您可以更轻松地知道它处于什么状态。
关于制作其他字段volatile
:
仅仅对访问所述字段的方法进行同步是不够的,因为它们可能会返回缓存值。
如果您访问同步块之外的值,您只会看到缓存值。
所有访问都需要正确同步。保证一个同步块的结束发生在另一个同步块开始之前(在同一监视器上同步时)。
至少有几种情况您仍需要使用同步:
如果您必须读取并以原子方式更新一个或多个字段,您可能希望使用同步。 您可以避免某些单个字段更新的同步,例如如果您可以使用Atomic*
类而不是“普通旧字段”;但即使对于单个字段更新,您仍可能需要独占访问权限(例如,将一个元素添加到列表中,同时删除另一个元素)。
此外,volatile/final 可能不足以用于非线程安全值,例如 ArrayList
或数组。
【讨论】:
【参考方案3】:如果一个对象在线程之间共享,您有两个明确的选择:
1.将该对象设为只读
因此,更新(或缓存)没有影响。
2。在对象本身上同步
缓存失效很难。 Very hard. 因此,如果您需要保证没有过时的值,您应该保护该值并保护该值周围的锁。
在共享对象上将锁和值设为私有,所以这里的操作是一个实现细节。
为了避免死锁,这个操作应该是“原子的”,以避免与其他任何锁交互。
【讨论】:
以上是关于Java:使所有字段成为最终字段或可变字段?的主要内容,如果未能解决你的问题,请参考以下文章