当前位置: 首页 > 面试题库 >

构造函数和指令重新排序

朱起运
2023-03-14
问题内容

我只是碰到一篇文章,声称我从未听过,也找不到其他地方。声称是从另一个线程的角度来看,可以根据构造函数内部的指令对构造函数返回的值的分配进行重新排序。换句话说,声称是在下面的代码中,另一个线程可以读取a其中x未设置的非空值。

class MyInt {
   private int x;

   public MyInt(int value) {
      x = value;
   }

   public int getValue() {
      return x;
   }
}

MyInt a = new MyInt(42);

这是真的?

编辑:

我认为从线程执行的角度来看,可以保证MyInt a = new MyInt(42)的分配与的分配x具有先发生后关系a。但是,这两个值都可能缓存在寄存器中,并且可能未按照最初写入的顺序将它们刷新到主存储器。因此,如果没有内存屏障,则另一个线程可以在写入的值a之前读取的值x。正确?

因此,基于axtavt的回答和随后的评论,这些线程安全性评估正确吗?

// thread-safe
class Foo() {
   final int[] x;

   public Foo() {
      int[] tmp = new int[1];
      tmp[0] = 42;
      x = tmp; // memory barrier here
   }
}

// not thread-safe
class Bar() {
   final int[] x = new int[1]; // memory barrier here

   public Bar() {
      x[0] = 42; // assignment may not be seen by other threads
   }
}

如果那是正确的…哇,那真的很微妙。


问题答案:

您引用的文章在概念上是正确的。正如您的问题一样,它的术语和用法有些不准确,这可能导致潜在的误解和误解。似乎我在这里使用术语,但是Java内存模型非常微妙,如果术语不够精确,那么一个人的理解就会受到影响。

我将从您的问题(和评论)中摘录要点,并提供答复。

可以根据构造函数内部的指令对构造函数返回的值的分配进行重新排序。

几乎可以,不是指令,而是可以重新排序的 内存操作
(读取和写入)。线程可以按特定顺序执行两条写指令,但是数据到达内存的方式以及这些写给其他线程的可见性可能以不同的顺序发生。

我认为从线程执行的角度来看,可以保证MyInt a = new MyInt(42)的分配与的分配x具有先发生后关系a

再次,差不多。诚然,按 程序顺序 是,对的赋值x发生在对的赋值之前a。但是, before-before
是适用于所有线程的全局属性,因此就特定线程而言,谈论before-before是没有意义的。

但是,这两个值都可能缓存在寄存器中,并且可能未按照最初写入的顺序将它们刷新到主存储器。没有内存屏障,因此另一个线程可以在写入x的值之前读取a的值。

再一次,差不多。值可以缓存在寄存器中,但是部分内存硬件(例如缓存或写缓冲区)也可能导致重新排序。硬件可以使用多种机制来更改顺序,例如缓存刷新或内存屏障(通常不会引起刷新,而只是防止某些重新排序)。但是,从硬件角度考虑这一问题的困难在于,实际系统非常复杂并且具有不同的行为。例如,大多数CPU具有几种不同的存储屏障类型。如果要考虑JMM,则应该考虑模型的要素:通过建立事前发生的关系来限制重新排序的内存操作和同步。

因此,为了从JMM的角度重新审视此示例,我们看到对字段x的写入和a对程序顺序的字段写入。在此程序中,没有什么限制重新排序,即没有同步,没有对volatile的操作,没有对最终字段的写入。这些写操作之间没有事前发生的关系,因此可以对它们进行重新排序。

有几种方法可以防止这些重新排序。

一种方法是进入x决赛。之所以可行,是因为JMM表示在构造函数返回 之前发生 写入最终字段之前-在构造函数返回之后 发生的 操作 之前
。由于a是在构造函数返回之后写入的,因此final字段的初始化x发生在写入之前a,并且不允许重新排序。

另一种方法是使用同步。假设MyInt实例在另一个类中使用,如下所示:

class OtherObj {
    MyInt a;
    synchronized void set() {
        a = new MyInt(42);
    }
    synchronized int get() {
        return (a != null) ? a.getValue() : -1;
    }
}

set()在写入xa字段之后,将在调用结束时进行解锁。如果另一个线程调用get(),它将在调用开始时锁定。这将在锁的释放(末尾)set()和锁的获取(开始时)之间建立事前关联关系get()。这意味着在调用开始之后,对写入的写入xa无法重新排序get()。因此,读取器线程将看到两者的有效值ax并且永远不会找到非null
a和未初始化的值x

当然,如果读取器线程get()较早调用,则它可能看似a为null,但是这里没有内存模型问题。

Foo和的Bar例子很有趣,您的评估基本上是正确的。在分配给最终数组字段之前发生的对数组元素的写入之后将无法重新排序。在分配给最终数组字段之后发生的对数组元素的写操作可能会相对于稍后发生的其他内存操作进行重新排序,因此其他线程的确可能会看到过时的值。

在评论中,您曾问过这是否有问题,String因为它具有包含其字符的最终字段数组。是的,这是一个问题,但是如果您查看String.java构造函数,则它们都非常小心,要在构造函数的最后将赋值给final字段。这样可以确保适当地可见数组的内容。

是的,这很微妙。:-)但是,只有当您尝试变得聪明时才真正出现问题,例如尝试避免使用同步或易变变量。大多数时候这样做是不值得的。如果您坚持“安全发布”的做法,包括this在构造函数调用期间不泄漏,以及使用同步存储对构造对象的引用(OtherObj例如上面的示例),那么事情将完全按照您的预期进行。

参考文献:

  • Goetz,《 Java并发实践》 ,第3章, 共享对象 。这包括对内存可见性和安全发布的讨论。
  • Manson / Goetz, Java内存模型常见问题解答 。http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html。有点旧,但有一些很好的例子。
  • Shipilev,Java内存模型实用程序。http://shipilev.net/blog/2014/jmm-pragmatics/。一位Oracle性能专家的演讲幻灯片和谈话记录。关于JMM的知识比您想了解的更多,还有一些指向Java未来版本中潜在的JMM修订版的指针。
  • OpenJDK 8 String.java源代码。http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/lang/String.java


 类似资料:
  • 变量res的值应等于3。但是当我打开优化时,编译器错误地重新排列了指令,并且res包含一些垃圾。一些可能的重新排序示例: 这是编译器中的错误吗?还是不允许像这样访问结构数据成员? 编辑: 我刚刚意识到之前的代码实际上有效,抱歉。但这不起作用: 当编译时不知道变量i时,编译器会错误地重新排序指令。

  • 问题内容: 我开始学习JAXB,所以我的问题可能很愚蠢。现在我有了类,想要生成XML Schema。在执行此指令后,我得到了异常 IllegalAnnotationExceptions …没有无参数的默认构造函数。 是的 我的课程没有默认的无参数构造函数。太容易了 我有类与包可见的构造函数/ final方法和偏离路线的参数。我应该怎么做-创建一些特定的momemto / builder类或将我的构

  • 问题内容: 我正在阅读此博客文章。 作者正在谈论在多线程环境中打破in 。 有了: 变成: 作者说,我引用: “我在这里所做的是添加一个附加读取: 哈希的第二次读取,在返回之前 。听起来很奇怪,而且不太可能发生,第一次读取可以返回正确计算出的哈希值,内存模型允许第二次读取返回0!这是允许的,因为该模型允许对操作进行广泛的重新排序。第二次读取实际上可以在代码中移动,以便处理器在第一次读取之前进行处理

  • 类构造函数 类constructor是类的特殊成员函数,只要我们创建该类的新对象,就会执行该函数。 构造函数与类具有完全相同的名称,它根本没有任何返回类型,甚至无效。 构造函数对于为某些成员变量设置初始值非常有用。 下面的例子解释了构造函数的概念 - #include <iostream> using namespace std; class Line { public: voi

  • 类构造函数 类constructor是类的特殊成员函数,只要我们创建该类的新对象,就会执行该函数。 构造函数与类具有完全相同的名称,并且根本没有任何返回类型,甚至无效。 构造函数对于为某些成员变量设置初始值非常有用。 以下示例解释了构造函数的概念 - import std.stdio; class Line { public: void setLength( double l

  • 本文向大家介绍解析C++中构造函数的默认参数和构造函数的重载,包括了解析C++中构造函数的默认参数和构造函数的重载的使用技巧和注意事项,需要的朋友参考一下 C++构造函数的默认参数 和普通函数一样,构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值。 【例】 程序运行结果为: 程序中对构造函数的定义(第12-16行)也可以改写成参数初始化