同步块:嵌套变量是否会被更新?

4
抱歉标题不够技术化,但我认为它很好地总结了我的问题。如果我正确理解所读的内容,同步块(除其他后果外)将使所有变量更新到/从主内存中(即使那些在同步块中未明确访问的“父级”变量)。例如,引用this stackoverflow问题的答案(我将其摘出上下文,稍后再回到它):
内存屏障适用于所有内存引用,即使是无关的引用。
我需要确认我是否正确解释了这个问题。我有2个线程(线程A,线程B)。考虑以下代码:
public class SomeClass {

private final Object mLock = new Object();
private int[] anArray;

public void initA() {
  synchronized(mLock) {
     ...
     anArray = new int[...];
     operationA();
  }
}

public void operationA() {
  synchronized(mLock) {
      // Manipulating the ELEMENTS of anArray,
      // e.g. in loops, etc.
      anArray[i] = ...
  }
}

public int[] getterB() {
   synchronized(mLock) {
      return anArray;   
   }
}
}
getterB() 是从 ThreadB 调用的,initA()operationA() 是从 ThreadA 调用的。(请注意,initA() 甚至在创建 ThreadB 之前就被调用了,因此只有 getterB()operationA() 是并发的。) 还要注意,我有充分的理由不返回数组的 副本getterB() 中(不,threadB 不想 更改 其元素;原因是我的软件对外部要求,现在与此无关)。 threadB 执行以下操作:
int[] anArray = aSomeClass.getterB(); // aSomeClass is an instance of SomeClass
if (anArray[i] == n) { ....... } // various operations
...
//  various other operations that read the elements of anArray

正如您所看到的,在getterB()中,只有anArray引用在内存屏障中被访问,而不是数组值本身。我的问题是:
  1. threadB会看到最新的数组元素值吗?(即,在getterB()中,这些元素本身是否也从主内存中更新了?)

  2. 引用的语句提到不相关的缓存副本也会从主内存中更新。我不确定如何解释这个“不相关”(是与用于锁定的变量无关还是与整个同步块无关?)。我知道我摘录了这句话,因为它是一个不同的stackoverflow问题,所以我添加了一个注释there。因此,如果我的问题在那里得到回答(或者在这里-我不在乎),我会很感激。

  3. 如果anArray对象数组(而不是原始类型数组),答案有什么区别吗?更进一步,如果它不是数组,而是一个包含对其他类的引用的类呢?(即,一个引用其他对象的对象,并且通过getterB()返回的对象访问这些包含的对象)。threadB会使用这些包含引用的最新副本,还是可能使用自己的本地缓存副本(因为getterB()只更新了它们的容器对象,而不是这些包含的引用本身)?

1个回答

4

按顺序回答您的问题:

  1. 是的:您可以安全地假设从先前调用的operationA()修改的所有值在由getterB()引用的数组中“最新”。

  2. 我会让其他链接回答这个问题;我承认我还没有阅读那个链接。但是我的理解是,当您进入和退出同步块时,所有待处理的内存写回将“有效”发生(尽管如何发生的详细信息 - 即,这种方式的效率以及是否有更多的缓存/流水线“技巧”使其看起来像这样 - 将取决于硬件和编译器)。如果您想了解更多关于此的详细信息,我曾经发现这个链接很有用:http://www.infoq.com/articles/memory_barriers_jvm_concurrency

  3. 不,没有区别(根据我在第二个答案中写的内容)。

最后,我想说的是,根据上述总结,我会非常警惕你的代码,因为getterB()并不返回数组的副本。我理解你有自己的原因来这样写(而且你可能并不希望得到这种反馈!),但你应该确保了解在getterB()返回后,在线程B中对anArray进行的所有“各种操作”都不受保护。(换句话说,在此期间在线程A上对数组所做的任何更改都会存在危险。)一个避免低效深层复制数组的替代方案是,在SomeClass中的新方法内部使用同步块来移动这些“各种操作”,并完全摆脱getterB()。当然,我意识到你的代码中的“正确解决方案”取决于许多未在此处显示的因素,所以请随意忽略这一点。

谢谢(我会再仔细阅读你的帖子,但我现在很匆忙)。关于你最后的观点:是的,你说得对,但幸运的是,由于我的应用程序具有更高级别的同步(事件顺序),operationA() 只有在 threadB 完成这些“各种操作”之后才会被调用。 - Thomas Calc
现在更加清晰了,而且也回答了我的具体问题。在另一个stackoverflow主题中,我也从Peter Lawrey那里得到了答案。只有一个最后的问题困扰着我,但由于需要代码,我很快就会在新的stackoverflow问题中提出它。 - Thomas Calc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接