【问题标题】:Object creation (state initialisation) and thread safety对象创建(状态初始化)和线程安全
【发布时间】:2013-10-04 11:18:46
【问题描述】:

我正在查看“Java Concurrency in Practice”一书,发现下面引用的陈述真的很难相信(但不幸的是它是有道理的)。

http://www.informit.com/store/java-concurrency-in-practice-9780321349606

只是想弄清楚这 100%

public class Holder {
    private int n;
    public Holder(int n) { this.n = n; }
    public void assertSanity() {
      if (n != n)
       throw new AssertionError("This statement is false.");
      }
}

虽然在构造函数中设置的字段值似乎是第一个 写入这些字段的值,因此没有“旧” 值被视为陈旧的值,首先是 Object 构造函数 将默认值写入子类之前的所有字段 构造函数运行。因此可以看到默认值 将字段作为陈旧值

关于上面的粗体声明,

我知道这种行为,但现在很明显,构造函数的这种调用层次结构不能保证是 ATOMIC(在由锁保护的单个同步块中调用超级构造函数),但解决方案是什么?想象一个具有多个级别的类层次结构(即使不推荐,让我们假设它是可能的)。上面的代码片段是我们在大多数项目中每天都能看到的一种原型。

【问题讨论】:

  • Java 中的多重继承是不可能的。在具有多重继承的语言中,有一些结构可以解决在两个或多个超类中具有相同字段的歧义。如果我们在讨论 Java,我仍然不明白“保证是 ATOMIC”是什么意思?
  • @M.Sameer 编辑了关于继承的正确措辞。意思是阶级层次不齐
  • 上面的例子说明了什么?你是说你可能会从中得到 AssertionError 吗?这对我来说很难相信。
  • @JanZyka 这就是书中所说的,该声明是从书中引用的

标签: java inheritance concurrency


【解决方案1】:

你误读了这本书。它明确地说:

这里的问题不是Holder类本身,而是Holder没有正确发布。

所以上面的构造如果没问题。不好的是不正确地将这样的对象发布到其他线程。这本书详细解释了这一点。

【讨论】:

  • 这令人困惑。正如在引用的statemnt中所说,问题的原因是由于构造函数调用的顺序“对象构造函数首先在子类构造函数运行之前将默认值写入所有字段。因此可能会看到一个默认值field as a stale value" 而这不在程序员的控制范围内,将归档 n 设为 final 可能是一种解决方案(一种解决方法),但仍然可以解决它真正导致的问题,即构造函数调用的顺序和他们的intilisation
  • 不是,问题的原因是对象发布到其他线程不正确,让其他线程查看对象的状态不一致。如果您不使该对象可用于其他线程,或者您通过正确发布它(如书中所述)正确地做到了这一点,那么您将永远不会遇到问题。 volatile、synchronized 和其他安全线程同步技术是解决方案。
  • 能否请您提供例如这种情况可能发生的情况
  • 书上的例子: 实际上,如果清单 3.15 中的 Holder 是使用清单 3.14 中的不安全发布惯用语发布的,并且发布线程以外的线程调用 assertSanity,它可以抛出 AssertionError!
【解决方案2】:

当创建一个新对象时,事情是按顺序发生的。我不知道确切的顺序,但它类似于:分配空间并将其初始化为零,然后设置获取常量值的字段,然后设置获取计算值的字段,然后运行构造函数代码。而且,当然,它必须在某个地方初始化子类。

因此,如果您尝试使用仍在构建的对象,您会在字段中看到奇怪的无效值。这通常不会发生,但有办法做到这一点:

  • 在分配给另一个字段的过程中引用一个还没有值的字段。

  • 在构造函数中引用一个直到稍后在构造函数中才被分配的值。

  • 在刚刚从 ObjectInputStream 读取的对象中的字段中引用对象中的字段。 (OIS 通常需要很长时间才能将值放入它读取的对象中。)

  • 在 Java 5 之前,类似于:

    public volatile MyClass  myObject;
    ...
    myObject = new MyClass( 10 );
    

    可能会造成麻烦,因为另一个线程可以在 MyClass 构造函数完成之前获取对 myObject 的引用,并且它会在对象内部看到错误的值(在本例中为 0 而不是 10)。在 Java 5 中,在构造函数完成之前,JVM 不允许将 myObject 设为非 null。

  • 今天你仍然可以在构造函数中将 myObject 设置为 this 并完成同样的事情。

如果你够聪明,你还可以在 Class 字段被初始化之前获取它们。

在您的代码示例中,如果在两次读取 n 之间的值发生了变化,(n != n) 就会为真。我想重点是n 从零开始,构造函数将get 设置为其他值,并在构造过程中调用assertSanity。在这种情况下,n 不是易失的,所以我认为断言永远不会被触发。让它变得不稳定,如果你把所有事情都准确地计时,它就会每百万次左右发生一次。在现实生活中,此类问题经常发生,足以造成严重破坏,但很少发生,以至于您无法重现它。

【讨论】:

  • "在 Java 5 中,在构造函数完成之前,JVM 不允许使 myObject 非空。"这仅在标记为 volatile 时有效吗?
  • @nish1013 是!!! 这里有一种内存障碍。当您写入 volatile 离开同步块时,您的所有内存更改必须在该点上对下一个读取 volatile 或同步的同一监视器上的任何线程可见. (在 Java 5 中为 volatile 添加了“所有内存更改”部分。)(还有一些不太常见的事情也会导致内存更改。)通常,更改必须出现发生在您仅在单个线程中编写它们的顺序,但可以以任何顺序出现在不同的线程中。
  • “同一个监视器”是指同一个对象吗?
  • @nish1013 是的。它是synchronized 语句中的对象。
【解决方案3】:

我想理论上是可能的。类似于双重检查锁定问题。

public class Test {
    static Holder holder;

    static void test() {
        if (holder == null) {
            holder = new Holder(1);
        }
        holder.assertSanity();
    }
...

如果 test() 被 2 个线程调用,线程 2 可能会在初始化仍在进行时看到持有者处于某个状态,因此 n != n 可能恰好为真。这是 n != n 的字节码:

ALOAD 0
GETFIELD x/Holder.n : I
ALOAD 0
GETFIELD x/Holder.n : I
IF_ICMPEQ L1

如您所见,JVM 将字段 n 加载到操作数堆栈两次。因此可能会发生第一个 var 在 init 之前获得值,而在 init 之后获得第二个值

【讨论】:

  • 真的吗?我是在问,不是说不是。我了解线程可能会看到不同的对象或未初始化的对象。但我希望线程只看到单个值(默认或通过构造函数设置),因此n != n 总是返回false
  • 但是您的代码显示静态实例和方法,与问题中发布的代码不同。
  • 它仍然使用Holder实例和实例方法holder.assertSanity()
【解决方案4】:

评论:

Object 构造函数首先将默认值写入所有字段 在子类构造函数运行之前

似乎错了。我之前的经验是,类的默认值是在其构造函数运行之前设置的。那是一个超类将在其构造函数运行并执行操作之前看到其初始化变量设置。这是错误的根源,一位朋友查看了基类在构造期间调用超类实现的方法的位置,并将在超类中使用初始化定义的引用设置为 null。该项目将一直存在,直到进入构造函数,此时 init 将其设置为空值。

在完成构造并返回对象引用之前,另一个线程无法获得对该对象的引用(假设在构造函数中没有生成)。

【讨论】:

  • 这是有保证的行为吗? JLS 的任何参考资料?
  • 您在我的评论的哪一部分询问第二段中的引用或第一段中的初始化行为? (或者你想要两者的参考?
猜你喜欢
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
  • 1970-01-01
  • 2011-02-26
  • 1970-01-01
  • 2010-11-29
  • 1970-01-01
  • 2012-05-22
相关资源
最近更新 更多