【问题标题】:java - what does synchronized really do according the java memory model?java - 根据java内存模型,同步真正做了什么?
【发布时间】:2017-12-31 10:58:39
【问题描述】:

看了一点java的内存模型和同步之后,提出了几个问题:

即使线程 1 同步写入,虽然写入的效果会被刷新到主内存,但线程 2 仍然看不到它们,因为读取来自 1 级缓存。所以同步写入只能防止写入冲突。 (Java thread-safe write-only hashmap)

其次,当一个同步方法退出时,它会自动与任何后续对同一对象的同步方法调用建立起之前的关系。这保证了对象状态的更改对所有线程都是可见的。 (https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html)

第三个网站(我再也找不到它了,抱歉)说,对任何对象的每次更改——它不关心引用来自哪里——都会在方法离开同步块并建立时刷新到内存中发生在的情况。

我的问题是:

  1. 退出同步块后真正刷新回内存的是什么? (正如一些网站也说过,只有获得锁的对象才会被刷新。)

  2. 在这种情况下,happens-before-relaitonship 是什么意思?进入区块时会从内存中重新读取什么,什么不是?

  3. 锁是如何实现这个功能的(来自https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html):

    所有 Lock 实现必须强制执行与内置监视器锁提供的相同内存同步语义,如 Java™ 语言规范第 17.4 节所述:

    成功的锁定操作与成功的锁定操作具有相同的内存同步效果。 成功的解锁操作与成功的解锁操作具有相同的内存同步效果。 不成功的锁定和解锁操作,以及重入锁定/解锁操作,不需要任何内存同步效果。

如果我认为所有内容都将被重新读取和刷新的假设是正确的,那么这可以通过在锁定和解锁功能中使用同步块来实现(这通常也是必需的),对吧?如果错了,这个功能怎么实现?

提前谢谢你!

【问题讨论】:

  • 如果你想知道 Java 内存模型是如何工作的,你应该强烈考虑阅读Java memory model
  • 同步块在块末尾添加一个full membar,并强制在处理进一步指令之前完全刷新 CPU 写入缓存。
  • "这是通过在锁定和解锁功能中使用同步块来实现的(大多数情况下也是必需的)" - 你弄错了。 A synchronized-block uses the intrinsic object-lock.
  • 等等,什么?你的意思是每个对象持有的锁,对吧?我说的是 java.util.concurrent.locks.Lock 的自定义实现,而不是监视器监督的对象锁。 (或者我是不是理解错了什么?)或者更具体地说:java.util.concurrent.locks.Lock 的实现如何确保与 synchronize-blocks 保证的内存功能相同?
  • 看看source code@Quaffel ...

标签: java concurrency synchronized memory-model


【解决方案1】:

happens-before-relationship 是您必须了解的基本内容,因为 the formal specification 就是根据这些来运作的。 “冲洗”之类的术语是技术细节,可以帮助您理解它们,或者在最坏的情况下误导您。

如果一个线程在synchronized(object1) { … } 中执行操作A,然后在synchronized(object1) { … } 中执行操作B,假设object1 引用同一个对象,则会发生 -AB 之间的-before-relationship,并且这些操作对于访问共享可变数据是安全的(假设没有其他人修改此数据)。

但这是有向关系,即B可以安全地访问A修改的数据。但是当看到两个synchronized(object1) { … }块时,确定object1是同一个对象时,你仍然需要知道A是在B之前执行的还是BA之前执行的,才能知道方向发生在关系之前。对于普通的面向对象的代码,这通常很自然,因为每个动作都会对其找到的对象的任何先前状态进行操作。

说到刷新,离开synchronized 块会导致所有写入数据的刷新,进入synchronized 块会导致重新读取所有可变数据,但在同一实例上没有synchronized 的互斥保证,有无法控制哪个发生在另一个之前。更糟糕的是,您无法使用共享数据来检测情况,因为在不阻塞其他线程的情况下,它仍然可以不一致地修改您正在操作的数据。

由于在不同对象上同步无法建立有效的 happens-before 关系,因此不需要 JVM 的优化器来维持全局刷新效果。最值得注意的是,如果逃逸分析证明该对象从未被其他线程看到,那么今天的 JVM 将取消同步。

因此,您可以在对象上使用同步来保护对存储在其他地方(即不在该对象中)的数据的访问,但是对于对相同共享数据的所有访问,它仍然需要在同一个对象实例上进行一致的同步,这会使程序复杂化逻辑,而不是简单地在包含受保护数据的同一对象上同步。


volatile变量,就像Locks内部使用的一样,也有全局flush作用,如果线程在读写同一个volatile变量,并使用该值形成正确的程序逻辑。这比 synchronized 块更棘手,因为代码执行没有互斥,或者您可以将其视为仅限于单个读取、写入或 cas 操作的互斥。

【讨论】:

  • 这让我想起了 Shipilev 上一次关于 Java 内存模型的演讲;当然,我们甚至不应该想知道这些细节,就像你自己说的 misguide you in the worst case,但我知道还有像我这样的人觉得这很有趣,根本不可能不去想“冲洗” "。
  • 感谢您的详细分析!根据内存模型,我只剩下一个问题:“如果 x 和 y 是同一个线程的动作,并且 x 在程序顺序中位于 y 之前,那么 hb(x, y)。”但是为什么允许编译器更改代码的顺序,这不会损害该规则吗?我的假设是:实际上允许编译器更改代码的顺序,但前提是这不会损害 program order -> 交换的操作不允许有关系(或至少是一个松散的,比如foo = new Foo(),所以 foo 的值可能会在对象完成之前被赋值)
  • @Eugene 我有同样的问题:我应该如何接受关于实现问题的答案(并且内存模型实际上是一个实现问题,因为它只是由于多核架构所必需的cpus)这与实施本身无关?人类希望彻底解决一个问题,而不仅仅是通过遗漏一些东西/创造一个新问题来解决一个问题。
  • 澄清一下我的第二个问题:是的,我知道我几乎是在自己回答我的问题,但我想知道我的结论是否以及我对程序顺序的理解 是对的。
  • @Quaffel 确切地说 - 显然有规则可以进行哪些优化,哪些不能进行优化,顺便说一句,这就是障碍(易失性/锁)的用途 - 禁止那些可能由编译器、CPU 等完成的可能重新排序
【解决方案2】:

没有 flush 本身,只是这样想更容易(也更容易绘制);这就是为什么网上有很多资源提到刷新到主内存(假设是 RAM),但实际上它并不经常发生。真正发生的是,将加载和/或存储缓冲区耗尽到 L1 缓存(IBM 的情况下为 L2),并且由缓存一致性协议从那里同步数据;或者换一种说法,缓存足够聪明,可以相互通信(通过总线),并且不会一直从主内存中获取数据。

这是一个复杂的主题(免责声明:即使我尝试对此进行大量阅读,有时间进行大量测试,但我绝对不能完全理解它),它是关于潜在的编译器/cpu /etc 重新排序(从不尊重程序顺序),它是关于缓冲区的刷新,关于内存屏障,释放/获取语义......我认为如果没有博士报告,你的问题是无法回答的;这就是为什么JLS 中有更高的层被称为 - “happens-before”。

至少了解上述内容的一小部分,您就会明白您的问题(至少前两个)没有什么意义。

退出同步块真正刷新回内存的内容

可能什么都没有 - 缓存彼此“对话”以同步数据;我只能想到另外两种情况:当您第一次读取一些数据时以及当一个线程死亡时 - 所有写入的数据都将被刷新到主内存(但我不确定)。

在这种情况下,发生关系之前是什么意思?进入区块时会从内存中重新读取什么,什么不是?

真的,和上面那句话一样。

锁是如何实现这个功能的

通常通过引入内存屏障;就像挥发物一样。

【讨论】:

  • 说实话:如果没有您和 Holger 的帮助,我无法想象理解这个极其复杂的主题(至少是部分地)——似乎 JLS 要求用户对多线程有广泛的了解. (这也可能是为什么有这么多网站更简化 JLS 的原因......)
猜你喜欢
  • 2012-01-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-13
相关资源
最近更新 更多