0

我有这样的代码:

if(!flag) {
   synchronized(lock) {
     lock.wait(1000);
   }
}
if(!flag) { print("Error flag not set!"); }

和:

void f() {
   flag = true;
   synchronized(lock) {
      lock.notify()
   }
}

我的一个朋友告诉我我应该把 flag = true 放在同步块中:

  synchronized(lock) {
      flag = true;
      lock.notify()
   }

我不懂为什么。这是一些经典的例子吗?有人可以解释一下吗?

如果我声明我的标志 volatile 我不需要将它放入同步块中?

4

4 回答 4

3

由于flag变量被多个线程使用,因此必须使用某种机制来确保更改可见性。这确实是一般多线程中的常见模式。Java 内存模型不保证其他线程将永远看到flag.

这是为了允许现代多处理器系统采用优化,在这些系统中,始终保持高速缓存一致性可能代价高昂。内存访问通常比其他“通常”的 CPU 操作慢几个数量级,因此现代处理器竭尽全力尽可能地避免它。相反,经常访问的位置保存在小型、快速的本地处理器内存中——缓存。更改仅对缓存进行,并刷新在某些点到主存储器。这适用于一个处理器,因为内存内容不会被其他方更改,因此我们保证缓存内容反映内存内容。(嗯,这是一个过度简化,但从高级编程的角度来看,我相信这无关紧要)。问题是,一旦我们添加另一个处理器,独立更改内存内容,这种保证就会丢失。为了缓解这个问题,设计了各种(有时是详尽的 - 参见例如这里缓存一致性协议。不过,不出所料,它们需要一些簿记和处理器间通信开销。

其他一些相关的问题是写操作的原子性。基本上,即使更改被其他线程看到,也可能会部分看到。这在 java 中通常不是什么大问题,因为语言规范保证了所有写入的原子性。尽管如此,对 64 位原语 (longdouble) 的写入被明确称为被视为两个独立的 32 位写入:

出于 Java 编程语言内存模型的目的,对非易失性 long 或 double 值的单次写入被视为两次单独的写入:每个 32 位一半。这可能导致线程从一次写入中看到 64 位值的前 32 位,而从另一次写入中看到后 32 位。( JLS 17.7 )

回到有问题的代码......需要同步,并且synchronized块满足需要。不过,我发现制作这样的标志volatile更令人愉快的解决方案。净效果是相同的——可见性保证和原子写入——但它不会用小块混淆代码synchronized

于 2013-09-13T07:44:18.253 回答
3

主内存很慢。真的很慢。今天,您 CPU 中的内部缓存快了大约 1000 倍。出于这个原因,现代代码试图在 CPU 的缓存中保留尽可能多的数据。

主内存如此缓慢的原因之一是它是共享的。当您更新主内存时,所有 CPU 内核都会收到更改通知。另一方面,缓存是每个核心的。这意味着当线程 A 更新标志时,它只是更新自己的缓存。其他线程可能会或可能不会看到更改。

有两种方法可以确保将标志写入主存储器:

  1. 把它放在一个synchronized块中
  2. 声明它volatile

volatile优点是对标志的任何访问都将确保更新主存储器中标志的状态。当您在许多地方使用标志时使用它。

在你的情况下,你已经有了synchronized块。但在第一种情况下,第一种if可能是读取过时的值(即线程可能wait()即使标志已经是true)。所以你仍然需要volatile.

于 2013-09-13T07:47:24.363 回答
2

如果您正在检查和修改来自不同线程的标志,则至少需要声明它volatile,以便线程看到更改。

将检查放在同步块中也可以。

是的,它是并发中非常基本的东西,所以你应该确保你阅读了内存模型happens-before和其他相关主题。

于 2013-09-13T07:44:39.913 回答
0

首先:即使其他线程没有发送通知,lock.wait(1000) 也会在一秒钟后返回。

其次:您的朋友是对的,在这种情况下,您已经共享了由不同线程访问的数据,因此最好使用代码中的锁来保护对它的访问。

第三:将您的标志变量标记为易失性,以便不同的线程确保它们始终使用最后一个“写入”值

最后:我还将 if (!flag) 代码放在同步块中 -> 它也在访问标志变量...

于 2013-09-13T07:48:04.260 回答