java线程在创建外部对象之前访问它

Posted

技术标签:

【中文标题】java线程在创建外部对象之前访问它【英文标题】:java thread accessing outer object before it's created 【发布时间】:2014-04-15 08:34:11 【问题描述】:

是的,这是一个学术问题,我知道人们会抱怨我没有发布任何代码 但我真的对这个问题感到震惊,真的不知道从哪里开始。我非常感谢您的解释,也许还有一些代码示例。

如果一个对象构造函数启动了一个执行该方法的新线程 运行匿名内部类对象,这个新的 线程可以在它被访问之前访问它周围的外部对象 完全构造并且其字段完全初始化。你会怎么 防止这种情况发生?

【问题讨论】:

您在线程启动之前所做的任何事情都将按照您的预期被读取。在构造函数的末尾创建线程(或者最好让对象的创建者,调用者,启动一个线程) 【参考方案1】:

这被称为“泄露这个”。这里有代码

public class Test 

  // this is guaranteed to be initialized after the constructor
  private final int val;

  public Test(int v) 
    new Thread(new Runnable() 
      @Override public void run() 
        System.out.println("Val is " + val);
      
    ).start();
    this.val = v;
  


猜猜它会打印什么(可能,因为它是一个线程)。我使用了final 字段来强调对象在完全初始化之前被访问(final 字段必须在每个构造函数的最后一行之后明确分配

你如何恢复

您不想在构造函数中传递this。这也意味着您不想在同一个类(非静态、非私有)中调用非最终虚拟方法,并且不使用隐式链接到封闭类的内部类(匿名类是内部类)例如,因此他们可以访问this

【讨论】:

不错的答案,如果您回答自己的问题“猜猜它会打印什么”,我会投赞成票。 (一个随机值还是0?)我也没有得到“你不想在同一个类(非静态,非私有)中调用非最终虚拟方法”的部分:你能提供一个例子那个? @AndreiI 它根据线程调度程序打印 0 或 v。可以被子类覆盖的方法(即非静态非私有非final)被称为virtual(JVM 在运行时调度调用)。由于它们访问子类实例,它们可能在子类的构造函数之前运行。见this example【参考方案2】:

先想想单线程的情况:

每当您通过new 创建对象时,都会调用其构造函数,该构造函数(希望)在返回对该对象的引用之前初始化新对象的字段。也就是说,从调用者的角度来看,这个new几乎就像一个原子操作:

在调用new之前,没有对象。从new返回后,对象存在完全初始化。

所以一切都很好。


当多个线程发挥作用时,情况会略有变化。但我们必须仔细阅读您的报价:

...已完全构建,其字段已完全初始化。

关键点是fully。您问题的主题行是“创建之前”,但这里的意思不是在创建对象之前,而是在对象创建和初始化之间。在多线程情况下,new 不能再被认为是(伪)原子的(时间从左到右流动):

Thread1 --> create object --> initialize object --> return from `new`
                           ^
                           |
                           | (messing with the object)
Thread2 ------------------/

那么 Thread2 怎么会弄乱对象呢?它需要对该对象的引用,但由于 new 只会在创建和初始化之后返回该对象,这应该是不可能的,对吧?

嗯,不——有一种方法仍然可行——即如果线程 2 是在对象的构造函数中创建的。那么情况会是这样的:

Thread1 --> create object --> create Thread2 --> initialize object --> return from `new`
                                      |       ^
                                      |       |
                                      |       | (messing with the object)
                                       \-----/

由于 Thread2 是在对象创建之后创建的(但在它完全初始化之前),因此已经有一个对 Thread2 可以获取的对象的引用。一种方法是如果 Thread2 的构造函数显式地将对象的引用作为参数。另一种方法是为 Thread2 的 run 方法使用对象的非静态内部类。

【讨论】:

但是没有任何一种方式真的很安全,只要你的课程不是最终的!由于您的构造函数可以从子类中称为“super()”,并且您可以在该子类完全构造之前访问它的方法... 这就是为什么您永远不应该在未在 EventPump 线程上调用的 SwingUI 对象的构造函数中调用 setVisible(true) 的原因之一...... 你忘了回答这个问题:“你如何防止这种情况......?” @Andrei 没错,实际上 :-) 但我想这有点含蓄:不要做我回答中描述的事情,或者至少要意识到风险。 @Falco Inheritance 完全是另一个问题——我的回答集中在如何在对象的创建和初始化之间“从外部”干扰对象。【参考方案3】:

我会更改问题的标题,因为线程不是在访问自己,而是第二个访问第一个。我是说: 你有一个线程,创建一个对象。 在此对象的构造函数中,您声明了一个实现 Runnable 的匿名内部类。 在第一个线程的同一个构造函数中,您启动一​​个新线程来运行您的匿名内部类。 因此,您有两个线程。如果你想确保新线程在构造函数“完全结束”之前不做任何事情,我会在构造函数中使用一些锁。这样,第二个线程可以启动,但会等到第一个线程结束。

public class A 
    int final number;

    A() 
        new Thread(
            new Runnable() 
                public void run() 
                    System.out.pritnln("Number: " + number);
                
        ).start();
    number = 2;
    

【讨论】:

这不包括匿名类Runnable - 根据要求【参考方案4】:

我不完全同意 Pablos 的回答,因为这在很大程度上取决于您的初始化方法。

public class ThreadQuestion 

    public volatile int number = 0;

    public static void main(String[] args) 
        ThreadQuestion q = new ThreadQuestion();
    

    public ThreadQuestion() 
        Thread t = new Thread(new Runnable() 

        @Override
        public void run() 
            System.out.println(number);             
        
        );

        try 
        Thread.sleep(500);
         catch(Exception e) 
            e.printStackTrace();
        
        number = 1;     
        t.start();
    

当你

    t.start()放在最后,打印正确的数据。 将t.start()放在sleep命令前,会打印0 删除 sleep 命令并将 t.start() 放在它可以打印 1 的分配之前(无法确定)

在 3 上玩智力游戏。)您可以说 1 种简单数据类型的“微小”分配将按预期工作,但如果您创建数据库连接,它将无法获得可靠的结果。

不要犹豫,提出任何问题。

【讨论】:

有可能,但不一定会发生。您只需要更改睡眠时间,更一般地说,更改属性初始化和线程启动之间的代码 我试图强调某些结果是无法确定的。最初的问题让我们提出很多开放点,我们应该在哪个方向上进一步挖掘。 如果线程的start方法是构造函数的最后一行,你可以保证不会发生。 这里没有任何保证 - 您的 3 个场景都不是确定的。编译器可以按照它想要的任何方式重新排序这些语句,前提是运行语句的线程不会注意到差异(在这种情况下,它不会)。并且当使用非易失性变量时,您无法保证何时(或者即使!)写入 number=1 对其他线程可见。简而言之,你很幸运。 @ChrisHayes 感谢您的意见。我通过关键字 volatile 扩展了代码 sn-p。您介意更改我的示例以使其正常工作吗?【参考方案5】:

这样的情况?

public class MyClass  
    private Object something;
    public MyClass() 
        new Thread() 
            public void run() 
                something = new Object();
            
        .start();
    

根据使用的实际代码,行为可能会有所不同。这就是为什么构造函数应该小心,这样它们就不会调用非私有方法(子类可以覆盖它,允许在超类完全初始化之前从子类访问超类this)。虽然这个特定的示例处理单个类和一个线程,但它与引用泄漏问题有关。

【讨论】:

以上是关于java线程在创建外部对象之前访问它的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程——线程同步

Java 面试参考指南 — 同步

java--线程--习题集锦

java.lang.IllegalMonitorStateException:在等待()之前对象没有被线程锁定?

java 多线程操作数据库

Java同步—线程锁和条件对象