当前位置: 首页 > 知识库问答 >
问题:

如果不需要这个值,Java是否允许优化掉一个易变的读操作,并删除发生在同步之前的操作?

东方高洁
2023-03-14

下面的代码示例显示了一种常见的方法来演示由缺失的发生前关系引起的并发问题。

private static /*volatile*/ boolean running = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running) {
                // Do nothing
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
}

如果运行不稳定,则保证程序在大约一秒钟后终止。但是,如果运行易失性,则根本不保证程序会终止(因为在这种情况下,没有发生之前的关系或保证对正在运行的变量的更改的可见性),这正是我的测试中发生的情况。

根据JLS 17.4.5,还可以通过写入和读取另一个易失性变量running2来强制执行发生前关系,如以下代码示例所示。

private static boolean running = true;
private static volatile boolean running2 = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running2 || running) {
                // Do nothing
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
    running2 = false;
}

在每次循环迭代中读取易失性变量running2,当它在大约一秒钟后被读取为false时,由于发生之前的关系,也保证运行的变量随后被读取为false。因此,程序保证在大约一秒钟后终止,这正是我的测试中发生的情况。

但是,当我将变量running2的读取放入while循环内的一个空 statement中时,如下面的代码示例所示,程序不会在我的测试中终止。

private static boolean running = true;
private static volatile boolean running2 = true;
    
public static void main(String[] args) throws InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (running) {
                if (running2) {
                    // Do nothing
                }
            }
        }
    }.start();
    Thread.sleep(1000);
    running = false;
    running2 = false;
}

这里的想法是,对< code>running2的< code>volatile读取类似于编译器内存屏障:编译器必须使asm重新读取非< code>volatile变量,因为对< code>running2的读取可能与另一个线程中的释放操作同步。这将保证非易失性变量(如< code>running)中新值的可见性。

但我的JVM似乎没有这样做。这是编译器或JVM错误,还是JLS允许这样的优化,在不需要值时删除易失性读取?(它只控制一个空的 if body,因此程序行为不依赖于正在读取的值,而只依赖于创建一个发生之前的关系。

我认为JLS适用于源代码,因为running2易失性,读取变量的效果不应该因为优化而被删除。这是编译器或JVM的错误,还是有一个规范实际上允许这样的优化?

共有2个答案

谭畅
2023-03-14

这是JVM错误,还是JLS允许它在不需要值时删除易失性读取?

两者都不是。

根据 JLS,此执行是有效的。

第二个线程必须在读取running2==true后不久完成。
但是JLS不能保证一个线程中的写入在另一个线程中变得可见所需的时间。
因此,您的程序执行是有效的,因为它对应于writerunning2=false需要很长时间才能传播到另一个线程的情况。

顺便说一下,在我的java版本(OpenJDK 64位服务器VM(构建17.0.3 7-suse-1.4-x8664,混合模式)上,程序大约在1秒内完成
这也是一个有效的执行-这对应于写入running2=false更快地传播到第二个线程的情况。

PS你提到了“内存屏障”。
对于内存屏障,通常存在一些最大时间,之后它会被保证传播到其他线程。
但是JLS不会根据内存屏障进行操作,不必使用它们,实际上只保证这一点:

只要程序的所有结果执行产生可以由内存模型预测的结果,实现就可以自由生成它喜欢的任何代码

PSS如果你想看到JVM为你的程序生成的真正的汇编代码,你可以使用PrintAssembly。

赵景曜
2023-03-14

…JLS是否允许在不需要该值时删除易失性读取?(它只控制一个空的if体,因此程序行为不依赖于读取的值,只依赖于创建之前发生的。)

根据17.4。JLS的记忆模型:

内存模型描述程序的可能行为。一个实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行都会产生内存模型可以预测的结果。

因此,只要执行的结果是“合法的”,JLS实际上允许在运行时执行任何操作。

JLS的“执行结果”是指程序执行的所有外部操作:对文件和网络套接字的操作、各种系统调用(例如读取当前时间)等。
我相信17.4.9。JLS的可观察行为和非终止执行是关于这个的(或类似的东西)。

在您的示例中,唯一的外部操作是Hibernate1秒,因此您的程序可以“优化”为:

    public static void main(String[] args){
        Thread.sleep(1000);
    }

如果上面的答案是正确的,因为无限循环也是合法执行,那么,我想,你的程序可以“优化”到:

    public static void main(String[] args){
        while(true);
    }

再说一次:运行时被允许做任何事情,只要它执行与JLS允许的合法执行相同的外部动作。

为了更清楚地说明问题,让我们以法律执行为例。

通用算法

17.4 中描述了一般算法。JLS 的内存模型。

每个独立线程的动作必须由该线程的语义控制,除了每次读取的值由内存模型决定。

因此,我们假设每个线程中的操作都是按顺序逐个执行的
与单线程程序的唯一区别是,对于从多线程访问的变量,读取可能会返回“意外”值。

获取读取的所有可能值的规则是这样的:

非正式地说,如果没有发生,则允许读取r查看写入w的结果 - 在排序之前阻止该读取。

换句话说,读取一些变量会返回

  • 中对than变量的最后一次写入发生在顺序
  • 之前
  • 或对than变量的任何写入,与的读取无关

注意,算法不允许“优化”任何内容。

法律执行举例

现在,让我们将算法应用于我们的示例以查找合法执行。
(注意:为简单起见,我们将省略诸如意外错误和操作系统终止程序之类的情况)

主线程没有共享变量的读取,因此它的行为就像单线程程序一样。
它的行动:

new Thread(){...}.start();
Thread.sleep(1000);
running = false;
running2 = false;

第二个线程是一个具有2个读取的循环
因此我们得到了一系列操作:

read(running == true)
read(running2 == ?)
read(running == true)
read(running2 == ?)
...
read(running == false)

一旦读取运行false,序列就会结束。

根据JLS,读取可以返回什么值?< br >首先,我们注意到< code>running2是易失性的,这意味着对它的读取和写入是以全局顺序发生的(称为同步顺序),并且对该顺序中的所有线程都是可见的。< br >所以:

> < li >在第二个线程看到write < code > running 2 = false 之前: < ul > < Li > < p > < code > running 2 = = true < br >这是初始写入(唯一可见的写入)。

运行==true运行==false
对于每次读取运行

  • 初始写入(running=true发生在read
  • 之前
  • 写入running=false与读取之前发生的无关

所以每个read运行 == ? 可以随机返回两次写入中的任何一次。

Main Thread              Thread2                 

[...]                    [...]                   
  ↓ (happens-before)       ↓ (happens-before)    
running = false;         running2 == true;       
  ↓ (happens-before)       ↓ (happens-before)    
running2 = false;        running == true | false 
  • running2 == false
    This is the latest visible write.
  • running == false
    Because running2 == false
    => running2 = false happens-before running2 == false
    => transitively running = false happens-before running == false
    Main Thread              Thread2              
    
    [...]                                         
      ↓ (happens-before)                          
    running = false;                              
      ↓ (happens-before)     [...]                
    running2 = false;          ↓ (happens-before) 
      └--------------------> running2 == false;   
          (happens-before)     ↓ (happens-before) 
                             running == false;    
    

    总而言之,第二个线程的所有合法执行:

      < li >可以按以下顺序开始:
    read(running == true)
    read(running2 == true)
    [... repeat the fragment above ...]
    
    • either:
      ...
      read(running2 == false)
      read(running == false)
      
      ...
      read(running == false)
      

      如果您可以“优化”易失性读取,并且执行结果将与上述某些合法执行的结果相同,则此优化是合法的。

      关于注释中提到的AdvancedJMM_15_VolatilesAreNotFences测试。

      在我看来,这个测试并没有证明,如果值没有被使用,编译器可以删除一个易失性加载/存储

      AdvancedJMM_14_SynchronizedAreNotFences不同,因为它使用synchronized(new Object()){}-在relations之前没有共享变量,也没有发生。

      附言:@pveentjer评论中提到过:

      JVM的非规范性部分确实讨论了可见性;因此,在某个时候,更改应该对其他线程可见。

      有人有链接和引用来支持吗?< br >我在任何地方都找不到它,但是,正如Peter Cordes所指出的,知道Java(或者甚至只有一些JVM)不允许在易失性写的可见性方面有无限的延迟,这将是非常有用的。

 类似资料:
  • 教程http://tutorials.jenkov.com/java-concurrency/volatile.html说 如果读取/写入最初发生在对易失性变量的写入之前,则不能将从其他变量的读取和写入重新排序为在对易失性变量的写入之后发生。写入挥发性变量之前的读取/写入保证在写入挥发性变量之前“发生”。 什么是“写入挥发性变量之前”?这是否意味着以前的读/写在相同的方法中,我们正在写入易失性变量

  • 假设我有一组从客户机发送到服务器的请求ID。服务器的响应返回我发送的请求ID,然后我可以将其从哈希集中删除。这将以多线程的方式运行,因此多个线程可以在哈希集中添加和删除ID。然而,由于生成的ID是唯一的(从线程安全的源代码,比如现在的,它会针对每个新请求进行更新),是否需要是? 我认为这可能导致问题的唯一情况是遇到冲突,这可能需要对底层对象进行数据结构更改,但在这个用例中似乎不会发生这种情况。

  • 我对同步块有一些疑问。在提问之前,我想分享另一个相关帖子链接的答案,以回答相关问题。我引用彼得·劳里的同一个答案。 > <块引号> 同步确保您对数据有一致的视图。这意味着您将读取最新值,其他缓存将获得最新值。缓存足够智能,可以通过特殊总线相互通信(JLS不需要,但允许)该总线意味着它不必触摸主存储器即可获得一致的视图。 如果您只使用同步,则不需要Volatile。如果您有一个非常简单的操作,而同步

  • 今天早上,我发现了我的GitHub Actions BETA版邀请,并开始玩它,目的是迁移一些我目前在CircleCi上运行的简单构建、测试和部署管道。 我仍然在试图理解操作,但我心目中的流程是,在推动之后,工作流中的第一个操作将启动一个Docker容器。在这个容器中,我将运行一些简单的构建过程,比如最小化资产和移除人工制品。接下来的操作将在构建上运行一些测试。管道中的下一个操作将部署到许多环境中

  • 问题内容: 我正在尝试在用户空间中使用mmap读取“ mem_map”开始的物理内存。它是一个包含所有物理页面的数组。这是一台运行3.0内核的i386计算机。 代码是这样的: 我以此为根。输出为: 可以肯定的是,我搜索了问题并将以下行添加到我的/etc/sysctl.conf文件中: 但这也不起作用。 谁知道为什么不允许这样的mem_map操作,以及如何解决呢? 谢谢。 问题答案: 听起来好像内核

  • 问题内容: 我有一个程序()(基于Jedis ),它定期写入Redis HASH()。我还有一个定期执行的程序()(独立的JVM进程),在Redis事务中执行以下操作: 我的假设是,当program_2在下一次运行program_1时删除HASH(带有KEY_1)时,它将再次创建HASH。这样对吗 ? 问题答案: 是。Redis是单线程的,事务会阻塞直到它们完成为止,因此,如果program_2启