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线程在创建外部对象之前访问它的主要内容,如果未能解决你的问题,请参考以下文章