我有一个这样的代码:
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,那么我不需要将它放入同步块中吗?
答案 0 :(得分:3)
由于flag
变量由多个线程使用,因此必须使用一些确保更改可见性的机制。这确实是多线程中的常见模式。 Java内存模型不保证其他线程将看到flag
的新值。
这是为了允许现代多处理器系统采用的优化,其中始终保持高速缓存一致性可能是昂贵的。内存访问通常比其他“常规”CPU操作速度慢,因此现代处理器尽可能地避免使用它。相反,频繁访问的位置保存在小型,快速的本地处理器内存中 - 缓存。仅对缓存进行更改,并在某些点将刷新到主内存。这适用于一个处理器,因为内存内容不会被其他方更改,因此我们保证缓存内容反映内存内容。 (嗯,这是一个过于简单化,但从高级别的编程角度来看,无关紧要,我相信)。问题是,只要我们添加另一个处理器,独立更改内存内容,这种保证就会丢失。为了缓解这个问题,设计了各种(有时详细的 - 参见例如here)缓存一致性协议。不出所料,它们需要一些簿记和处理器间通信开销。
其他有些相关的问题是写操作的原子性。基本上,即使其他线程看到更改,也可能会看到部分。这通常不是java中的问题,因为语言规范保证了所有写入的原子性。仍然,写入64位原语(long
和double
)被明确地视为两个独立的32位写入:
出于Java编程语言内存模型的目的,对非易失性long或double值的单次写入被视为两个单独的写入:每个32位一半写入一次。这可能导致线程从一次写入看到64位值的前32位,而从另一次写入看到第二次32位的情况。 (JLS 17.7)
回到有问题的代码......需要同步,synchronized
块满足需要。尽管如此,我发现像volatile
这样的旗帜更令人愉快。净效果是相同的 - 可见性保证和原子写入 - 但它不会使代码与小synchronized
块混乱。
答案 1 :(得分:3)
主内存很慢。真的很慢。今天CPU中的内部缓存大约快了1000倍。出于这个原因,现代代码试图在CPU的缓存中保留尽可能多的数据。
主内存如此慢的一个原因是它是共享的。更新主内存时,会通知所有CPU内核更改。另一方面,缓存是每个核心。这意味着当线程A更新标志时,它只更新自己的缓存。其他线程可能会也可能不会看到更改。
有两种方法可以确保将标志写入主存储器:
synchronized
块volatile
volatile
的优点是,对该标志的任何访问都将确保更新主存储器中的标志状态。在许多地方使用旗帜时使用此功能。
在您的情况下,您已经拥有synchronized
块。但在第一种情况下,第一个if
可能正在读取陈旧值(即,即使该标志已经是wait()
,该线程也可能true
。所以你仍然需要volatile
。
答案 2 :(得分:2)
如果您正在检查并修改来自不同线程的标志,则必须至少声明volatile
,以便线程查看更改。
将检查放在同步块中也可以。
是的,这是并发中的一个非常基本的东西,所以你应该确保你阅读内存模型,happens-before
和其他相关主题。
答案 3 :(得分:0)
首先:即使其他线程没有发送通知,lock.wait(1000)也会在一秒后返回。
其次:您的朋友是对的,在这种情况下,您拥有不同线程访问的共享数据,因此最好使用代码中的锁来保护对它的访问。
第三:将您的标志变量标记为易失性,以便不同的线程确保它们始终使用最后的“写入”值
最后:我还将if(!标志)代码放在同步块中 - >它还访问了标志变量......