【问题标题】:volatile vs not volatile易失性与非易失性
【发布时间】:2017-05-06 19:08:34
【问题描述】:

让我们考虑下面这段 Java 代码

int x = 0;
int who = 1
Thread #1:
   (1) x++;
   (2) who = 2;

Thread #2
   while(who == 1);
   x++;   
   print x; ( the value should be equal to 2 but, perhaps, it is not* )    

(我不知道 Java 内存模型——假设它是强内存模型——我的意思是:(1)和(2)不会交换)
Java 内存模型保证对 32 位变量的访问/存储是原子的,因此我们的程序是安全的。但是,尽管如此,我们还是应该使用属性volatile,因为 *. x 的值可能等于1,因为x 可以在Thread#2 读取时保存在寄存器中。为了解决这个问题,我们应该将x 变量设为volatile。很清楚。

但是,那种情况呢:

    int x = 0;
    mutex m; ( just any mutex)
Thread #1:
       mutex.lock()
       x++;
       mutex.unlock()

    Thread #2
       mutex.lock()
       x++;   
       print x; // the value is always 2, why**?
       mutex.unlock()

x 的值始终为2,尽管我们没有设置为volatile。我是否正确理解锁定/解锁互斥体与插入内存屏障有关?

【问题讨论】:

  • 请发布 MCVE。
  • 什么是 MCVE?
  • 阅读它here
  • 在第一个 sn-p 中,允许 HotSpot 将循环优化为while (true),如果您让它旋转一段时间,它实际上会执行这样的优化。我认为该代码非常不安全。
  • 啊,stackoverflow 不允许包含指向 lmgtfy 的链接的评论。

标签: java multithreading shared-memory memory-barriers


【解决方案1】:

我会努力解决这个问题。 Java 内存模型有点复杂,很难包含在单个 StackOverflow 帖子中。请参阅 Brian Goetz 的 Java Concurrency in Practice 了解全文。

x 的值始终为 2,尽管我们不会让它变得易变。我是否正确理解锁定/解锁互斥体与插入内存屏障有关?

首先,如果你想了解 Java 内存模型,你想通读的总是Chapter 17 of the spec

该规范说:

监视器上的解锁发生在该监视器上的每个后续锁定之前。

所以是的,在您的显示器解锁时有一个内存可见性事件。 (我假设“互斥锁”是指监视器。java.utils.concurrent 包中的大多数锁和其他类也具有 happens-before 语义,请查看文档。)

Happens-before 是 Java 的意思,它不仅保证事件是有序的,而且保证内存可见性。

We say that a read r of a variable v is allowed to observe a write w
to v if, in the happens-before partial order of the execution trace:

    r is not ordered before w (i.e., it is not the case that 
    hb(r, w)), and

    there is no intervening write w' to v (i.e. no write w' to v such
    that hb(w, w') and hb(w', r)).

Informally, a read r is allowed to see the result of a write w if there
is no happens-before ordering to prevent that read. 

这一切都来自17.4.5。通读有点混乱,但如果你通读它,信息就在那里。

【讨论】:

  • @scottb 好吧,他似乎在询问互斥锁/监视器。 volatile 只是他的前提的一部分。当然,我们经常可以在这里回答具体的问题,但是要在 Java 中很好地编程,确实需要了解完整的内存模型。例如,很多人都知道volatile 做了什么,但似乎很少有人知道final 做了什么。 (提示:阅读规范的第 17 章!!)
【解决方案2】:

让我们回顾一些事情。以下陈述是正确的:Java 内存模型保证对 32 位变量的访问/存储是原子的。但是,这并不意味着您列出的第一个伪程序是安全的。仅仅因为两个语句在语法上是有序的,not 是否意味着它们更新的可见性也按照其他线程查看的顺序排列。线程 #2 可能会在 x 的增量可见之前看到由 who=2 引起的更新。使 x volatile 仍然不会使程序正确。相反,使变量 'who' 易变将使程序正确。那是因为 volatile 以特定方式与 java 内存模型交互。

我觉得在对 volatile 的常识理解的核心中存在一些“写回主存”的概念,这是不正确的。在 Java 中,Volatile 不会将值写回主内存。读取和写入 volatile 变量所做的是创建所谓的先发生关系。当线程 #1 写入 volatile 变量时,您正在创建一种关系,以确保查看该 volatile 变量的任何其他线程 #2 也将能够“查看”线程 #1 在此之前采取的所有操作。在您的示例中,这意味着使“谁”变得不稳定。通过将值 2 写入“谁”,您正在创建一个发生在之前的关系,以便当线程 #2 查看 who=2 时,它同样会看到 x 的更新版本。

在您的第二个示例中(假设您也打算拥有“谁”变量),互斥锁解锁创建了我在上面指定的发生之前的关系。因为这意味着查看互斥锁解锁的其他线程(即他们能够自己锁定它)他们将看到 x 的更新版本。

【讨论】:

  • “被其他线程查看”是这里的关键。 within 语句保证按程序顺序(它们在代码中出现的顺序)执行。 其他线程看到的内存写入的可见性根本无法保证。需要 Mutex 或 volatile 才能就线程 2 在“可能”之外看到的内容做出任何声明。
  • 还有“‘写回主存’的概念”:是的。 Java 是为不一定缓存一致的硬件编写的。 Brian Goetz 在 Java Concurrency in Practice 中谈到了 Java 设计的这一方面,并​​表示 Java 是为此类 CPU 设计的。这有点奇怪,但很容易习惯。但是,volatile 明确确实创建了“刷新到主内存”,这就是发生前发生的意思(以及其他一些事情)。
  • 从功能上讲,我不确定“刷新到主内存”是表达所发生情况的正确方式。我的理解是 volatile 通常被实现为内存屏障。这些构造不一定会导致 CPU 缓存刷新;它们只是限制了内存重新排序的发生方式,这会影响处理器“查看”值的方式。
  • 据我所知,大多数 CPU 实际上并不使用内存屏障。根据英特尔(我询问了他们的支持)的说法,他们有一个单独的总线,QPI 总线,它实际上交换内存读取和写入的信息,比主内存总线快得多。英特尔架构根本不需要实际的内存写入,并且缓存是一致的(在大多数操作中)。
  • @markspace JVM 仍然需要从寄存器中写回值,而没有“内存屏障”,它可以决定将其保留在寄存器中。
猜你喜欢
  • 1970-01-01
  • 2018-01-18
  • 1970-01-01
  • 2023-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-07
  • 2019-11-07
相关资源
最近更新 更多