读取可能同时写入的变量是否可以?
可能听起来有点傻,但我不精通 Java,所以想确保:
如果有两个代码点
一世:
if (_myVar == null)
{
return;
}
二:
synchronized (_myLock)
{
_myVar = MyVarFactory.create(/* real params */)
}
编辑:假设这_myVar是一个复杂的对象(即不是布尔值、整数或长整数)而是一个完全成熟的 java 类,它具有一些父类等。
假设 I 和 II 可以同时在不同的线程上运行,我认为在 C++ 中这将是“数据竞争”,但是我不确定 Java 中的情况。
回答
不,不是线程安全的。
数据竞争只是一个问题。您可能在CPU 核心 缓存可见性方面存在问题。
如果您的变量是 boolean、int 或 long(或等效的对象)类型,请使用适当的Atomic…类。这些类包装它们的内容以便线程安全访问。
如果不是这些类型,请使用AtomicReference以线程安全的方式包含您的对象。
除了Atomic…类之外,还有其他技术。
Stack Overflow 上已经多次介绍了所有这些内容。所以搜索以了解更多信息。
并阅读Brian Goetz 等人的经典著作Java Concurrency in Practice。
回答
TL;DR:不,不行。
解释:
相关文档是Java Memory Model (JMM)。
JMM 使 JVM 可以自由地为每个单独线程的所有对象上的每个字段制作本地缓存副本。
然后,它给每个线程一个硬币。每当线程读取一个字段或写入一个字段时,它就会翻转这个硬币。在头上,它使用其本地缓存。最后,它更新其本地缓存以及“真实”副本。
此外,硬币是邪恶的。它实际上不是随机的,但它是不可靠的。它可能会在今天每次、每次在测试机上以及在测试版的第一周内每次都翻转。然后,就在您向那个重要的潜在客户进行演示时,它开始可靠地、整天、每次都对您产生兴趣。只是……突然之间。
游戏的名字很简单:如果你的程序的行为取决于邪恶硬币翻转的结果,你就输了。
因此,要么编写不关心的代码(硬),要么编写抑制翻转的代码(更容易)。
在一般情况下,最容易做的事情是永远不会有任何的字段,你同时写入和读取。这听起来不可能,但实际上非常简单:像 fork join 这样的自顶向下框架通过堆栈进行所有通信(因此,方法参数传递和方法返回值),当然还有那个古老的、尝试过的、真正的技巧:通过对并发操作有出色支持的通道进行所有通信,例如像 postgres 这样的关系数据库,或者像 rabbitmq 这样的消息队列。
如果您必须以并发方式使用来自多个线程的相同字段,确保邪恶硬币不被翻转的唯一方法是建立所谓的“Happens-Before/Happens-After”关系(这是使用的官方术语)在 JMM 中):有一些特定的方法可以建立关系,这样 JMM 会正式祝福 2 行代码:那行肯定会“发生在”那行之后(这意味着:“发生在”之后的那行肯定会遵守由“之前发生”的行引起的更改)。如果没有 HBHA,就会发生邪恶的硬币翻转,您可能会或可能不会看到取决于月相的变化。
HBHA 因果关系的列表很长,但常见的方法是:
- 自然:在同一线程中运行的 2 位代码具有自然的 HBHA 关系。JVM/CPU 实际上可以自由地重新排序代码并同时运行,但 JVM 保证任何代码观察到的任何代码都好像单个线程中的代码严格按顺序运行一样。
- 启动线程:
thread.start()保证在该线程中的第一行代码之前发生。 synchronized:如果一个线程退出一个同步块,那么这发生在任何其他线程进入同步于同一对象引用的同步块之前。- volatile:对 volatile 字段的读/写建立任意顺序,但它是可靠的,并设置 HBHA。
在您的代码示例中,绝对没有 HBHA,因为我假设第一个代码段在一个线程中运行,而第二个代码段在另一个线程中运行。是的,第二个片段使用synchronized,但第一个片段没有,并且synchronized只能与其他synchronized块建立 HBHA (并且仅当它们在完全相同的对象上同步时)。因此,您没有 HBHA。
因此,JMM 使 JVM 可以自由地运行您的代码段,这样您就不会观察到第二个代码段(其中_myVar设置为某个实例)完成的更新,即使它可以观察到第二个线程确实更改的其他内容。
解决方案:设置HBHA;使用AtomicReference它为你做的,或者synchronized(_myLock)在第一个片段周围扔一个,或者忘记这一点并使用 db 或 rabbitmq 或 fork/join 或其他一些框架。
注意:几乎没有办法编写测试来确认发生了邪恶的抛硬币事件。您应该接受建议,考虑避免讨论完全使用 fork/join、消息队列或数据库在线程之间共享可变字段的必要性,因此:共享字段的多线程代码倾向于充满错误没有测试可以捕获。