【问题标题】:Does synchronized keyword prevent reordering in Java?synchronized 关键字是否会阻止 Java 中的重新排序?
【发布时间】:2014-06-10 01:00:02
【问题描述】:

假设我在 Java 中有以下代码

a = 5;
synchronized(lock){
    b = 5;
}
c = 5;

同步是否会阻止重新排序? a、b 和 c 之间没有依赖关系。会先分配给 a,然后再分配给 b,然后再分配给 c?如果我没有同步,语句可以以 JVM 选择的任何方式重新排序吗?

【问题讨论】:

  • 发生的任何事情都完全取决于 JVM,性能不会受到影响。为什么要查明是否已完成重新排序?同步在对象级别。
  • 在实际代码中,代替 (c=5),我返回一个值,另一个线程需要启动,如果第三条语句在第二条之前执行,代码将失败。我希望在几个语句之间没有重新排序,并想知道同步是否会阻止重新排序
  • 如果您需要 c = 5 对正在运行的线程可见,您必须将它放在同步块中。否则不能保证其他线程会看到这个值。
  • 甚至将c=5 放入synchronized 是不够的。读取变量c 的线程必须在synchronized 块内同步在同一对象上 以确保正确读取。但即使这样也只能保证读取线程在执行包含读取的synchronized 块时会读取更新的值 写入线程执行包含写入的synchronized 块。 synchronized 只保证没有“同时”,因此执行一个显着的命令。但仅此一项还不能说明会是哪个顺序。

标签: java multithreading synchronized


【解决方案1】:

将分配锁定到b 至少会在分配之前引入一个获取围栏,在分配之后引入一个释放围栏。

这可以防止将获取栅栏之后的指令移动到栅栏上方,以及将释放栅栏之前的指令移动到栅栏下方。

使用 ↓↑ 符号:

a = 5;
↓ 
b = 5;
↑
c = 5;

↓ 防止指令在其上方移动。 ↑ 防止指令被移到它下面。

【讨论】:

  • @WhoAmI 可能会发生一些重新排序。简而言之,有些指令可以移到同步块内,但任何指令都不能移出同步块。
  • @WhoAmI 我已经扩展了我的答案,希望现在更清楚。
  • 将代码移到块内有什么效果?
  • @WhoAmI 编译器、运行时或处理器可能会决定这样做以提高性能。如果您问“处理器如何知道在块内移动指令会提高性能?”,那么我不知道。
  • 优化的一个例子是将两个或多个相邻的synchronized 块合并为一个。如果这些块之间有一些指令,这也是可能的。但是停留在这个问题的例子中,假设abc 位于连续的内存位置,并且硬件能够在一次操作中传输多个ints(这不是太牵强)。那么执行 lock-transferall-unlock 而不是 transferone-lock-transferone-unlock-transferone 将是一种改进......
【解决方案2】:

同步是否会阻止重新排序?

它可以防止一些重新排序。您仍然可以在同步块之外和同步块内进行重新排序,但不能从同步块内部到同步块外部。

a、b 和 c 之间没有依赖关系。

这没什么区别。

会先赋值给 a 然后再赋值给 b 然后再赋值给 c 吗?

是的。但正如已经指出的,这并不能保证所有的 JVM。 (见下文)

如果我没有同步,语句可以以 JVM 选择的任何方式重新排序吗?

是的,由 JVM 和/或 CPU 指令优化器和/或 CPU 缓存,但鉴于没有明显的理由怀疑更改 a = 5; 的顺序,这不太可能; b = 5;将提高性能。

您可以看到缓存可见性的变化。即读取这些值的另一个线程可以看到 b = 5;在 a = 5 之前;例如它们位于不同的缓存行上,如果它没有同步的话。

【讨论】:

  • 据我所知,这是错误的。我们可以将尽可能多的额外写入/读取放入同步块中,并根据内部的正常规则重新排序。我们只需要确保不违反监视器的后续锁定和解锁之间的 hb 关系 - 即使我们重新排序所有三个写入也不会违反(只要保证在解锁之后锁定的下一个线程看到a = 5; b=5)
  • @Voo 这就是我所说的。在仅写入的情况下,读取 hb 不适用。正如 OP 所述,这些陈述只是示例。
  • 确实这是我对 JMM 的解读。我不会引用 JMM,而是引用 Doug Lea 的 this 和说明重新排序的第一个表。您会看到 MonitorExit, NormalStore 可以重新排序。或者,如果我们通过 JLS:解锁监视器和锁定它之间的 hb 关系仅保证如果发生 b=5,我们将看到 a=5,但它并没有说明我们看到 c 的值是什么。
  • 一种可能的实现,但即使在 Aarch64 上,CPU 仍然可以重新排序后面的写入以首先完成,并且无论如何编译器完全有权将给定代码更改为 synchronized(lock) { c=5; b = 5; a=5; } 在它之前甚至发出任何机器代码。我并不是说在一个特定 ISA 下的一个特定实现下这是行不通的,我是说它在一般情况下具有误导性。没有 volatile 的双重检查日志记录在大多数情况下也可以工作,但您仍然不建议这样做,对吧?
  • @Voo 我已添加澄清和建议以阅读您的 cmets。
【解决方案3】:

同步是否会阻止重新排序?

部分,见下文。

会先分配给 a 然后再分配给 b 然后再分配给 c 吗?

否。 正如 dcastro 指出的那样,动作可以移动到同步块中。因此编译器将被允许生成代码,对应于以下语句:

synchronized (lock){
    a = 5;
    b = 5;
    c = 5;
}

并且编译器还可以在同步块中重新排序语句,它们彼此之间没有依赖关系。所以编译器也可以生成对应以下语句的代码:

synchronized (lock){
    c = 5;
    b = 5;
    a = 5;
}

如果我没有同步,语句可以以 JVM 选择的任何方式重新排序吗?

嗯,我认为这是一个错误的问题,也是思考Java 内存模型的错误方式。 Java 内存模型不是根据重新排序定义的。实际上,这比大多数人想象的要容易得多。基本上,只有一条重要规则,您可以在 Java 语言规范的 §17.4.5 中找到:

当且仅当所有顺序一致的执行都没有数据竞争时,程序才正确同步。如果一个程序正确同步,那么该程序的所有执行将看起来是顺序一致的。

换句话说:如果您完全忽略重新排序,并且对于程序的所有可能执行,两个线程无法访问相同的内存位置,这既不是易失性 也不是atomic,并且至少有一个action是写操作,那么所有的执行都会出现,就好像没有reorderings一样。

简而言之:避免数据竞争,您将永远不会观察到重新排序

【讨论】:

  • +1,值得一提的是,正确的同步适用于同步在同一个锁实例上的线程,因此,一旦证明,JVM 可以完全忽略synchronized没有其他线程可以访问特定的锁实例。
猜你喜欢
  • 1970-01-01
  • 2017-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-25
  • 2013-01-18
  • 1970-01-01
相关资源
最近更新 更多