我是否需要将标志放入同步块中?

时间:2013-09-13 07:39:44

标签: java multithreading concurrency

我有一个这样的代码:

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

并且:

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

我的一个朋友告诉我,我应该在synchronized块中放置flag = true:

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

我不明白为什么。这是一些经典的例子吗?请有人解释一下吗?

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

4 个答案:

答案 0 :(得分:3)

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

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

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

  

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

回到有问题的代码......需要同步,synchronized块满足需要。尽管如此,我发现像volatile这样的旗帜更令人愉快。净效果是相同的 - 可见性保证和原子写入 - 但它不会使代码与小synchronized块混乱。

答案 1 :(得分:3)

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

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

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

  1. 将其放入synchronized
  2. 声明volatile
  3. volatile的优点是,对该标志的任何访问都将确保更新主存储器中的标志状态。在许多地方使用旗帜时使用此功能。

    在您的情况下,您已经拥有synchronized块。但在第一种情况下,第一个if可能正在读取陈旧值(即,即使该标志已经是wait(),该线程也可能true。所以你仍然需要volatile

答案 2 :(得分:2)

如果您正在检查并修改来自不同线程的标志,则必须至少声明volatile,以便线程查看更改。

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

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

答案 3 :(得分:0)

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

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

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

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