多线程场景下的数据可见性

3
另一种情况,基于之前的问题。我认为,其结论足够普遍以对广大受众有用。引用 Peter Lawrey 在此处的话:
同步使用内存屏障,确保该线程内所有内存处于一致状态,无论是在块内还是不在块内引用。
首先,我的问题仅涉及数据可见性。也就是说,原子性(“操作同步”)已在我的软件中得到了保证,因此在相同值上进行任何写操作之前,每个读操作都已完成,反之亦然,等等。因此,问题只涉及线程可能缓存的值。
考虑 2 个线程 threadA 和 threadB,以及以下类:
public class SomeClass {

private final Object mLock = new Object();    
// Note: none of the member variables are volatile.

public void operationA1() {
   ... // do "ordinary" stuff with the data and methods of SomeClass

     /* "ordinary" stuff means we don't create new Threads,
         we don't perform synchronizations, create semaphores etc.
     */
}

public void operationB() {
  synchronized(mLock) {
     ...
     // do "ordinary" stuff with the data and methods of SomeClass
  }
}

// public void dummyA() {
// synchronized(mLock) {
//    dummyOperation();
//  }
// }

public void operationA2() {
   // dummyA();  // this call is commented out

   ... // do "ordinary" stuff with the data and methods of SomeClass
}
}

已知事实(它们来自于我的软件架构):
- operationA1()operationA2()线程A调用,operationB()线程B调用。 - 在这个类中,operationB()线程B唯一调用的方法。请注意,operationB()在同步块中。 - 非常重要:保证以以下逻辑顺序调用这些操作:operationA1()operationB()operationA2()。保证每个操作在前一个操作被调用之前完成。这是由于更高级别的架构同步(消息队列,但现在不相关)。正如我所说,我的问题纯粹与数据可见性有关(即数据副本是否最新或过时,例如由于线程自己的缓存)。
根据Peter Lawrey的引用,operationB()中的内存屏障确保在operationB()期间,所有内存都处于一致状态,对于线程B。因此,例如,如果线程AoperationA1()中更改了某些值,则这些值将从线程A的缓存中写入主内存,到operationB()开始时。问题#1:这正确吗?
问题#2:当operationB()离开内存屏障时,由operationB()更改的值(可能由线程B缓存)将被写回主内存。但是,operationA2()不安全,因为没有人要求线程A与主内存同步,对吧?因此,即使operationB()的更改现在在主内存中,也无关紧要,因为线程A可能仍然具有其在调用operationB()之前的缓存副本。 问题 #3: 如果我的 Q.#2 中的怀疑是正确的,则请再次检查我的源代码并取消注释 dummyA() 方法,并取消注释 operationA2() 中对 dummyA() 的调用。我知道这可能在其他方面是不好的做法,但这有什么区别吗?我的(可能错误的)假设如下:dummyA() 将导致 threadA 更新其缓存数据来自主内存(由于 mLock 同步块),因此它将看到由 operationB() 进行的所有更改,即现在一切都是安全的。顺便说一下,方法调用的逻辑顺序如下:
  1. operationA1()
  2. operationB()
  3. dummyA()
  4. operationA2()

我的结论:由于 operationB() 中的同步块,threadB 将看到可能在之前被更改的数据的最新值(例如,在 operationA1() 中)。由于 dummyA() 中的同步块,threadA 将看到在 operationB() 中更改的数据的最新副本。这个思路有什么错误吗?

1个回答

2

关于问题2,您自己的直觉是正确的。在操作A2的开头使用synchronized(mLock)将发出一个内存屏障,这将确保操作A2进一步读取将看到由操作B执行的写入,这是由于操作B中使用synchronized(mLock)隐含的内存屏障而发布的。

然而,为了回答问题1,请注意,除非在操作A1的末尾插入完整的内存屏障(即,没有任何东西告诉系统从操作A1线程的缓存中刷新值),否则操作B可能看不到操作A1执行的任何写入。因此,您可能需要在操作A1的末尾调用dummyA。

为了完全安全和更易于维护,并且由于您声明这些方法的执行不会重叠,您应该在synchronized(mLock)块中封装所有共享状态的操作,而不会损失性能。


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