我一直在寻找为什么不应该在类的构造函数中调用线程的启动方法的理由。考虑以下代码:
class SomeClass
{
public ImportantData data = null;
public Thread t = null;
public SomeClass(ImportantData d)
{
t = new MyOperationThread();
// t.start(); // Footnote 1
data = d;
t.start(); // Footnote 2
}
}
ImportantData 是一些通用的东西(可能很重要),而 MyOperationThread 是知道如何处理 SomeClass 实例的线程的子类。
脚节点:
我完全理解为什么这是不安全的。如果 MyOperationThread 尝试在以下语句完成(并且数据已初始化)之前访问 SomeClass.data,我将得到一个我没有准备好的异常。或者也许我不会。你不能总是用线程来判断。无论如何,我都在为以后奇怪的、意想不到的行为做准备。
我不明白为什么这样做是禁区。至此,SomeClass的所有成员都已经初始化完毕,没有调用其他改变状态的成员函数,从而有效构造完成。
据我所知,这样做被认为是不好的做法的原因是您可以“泄漏对尚未完全构造的对象的引用”。但是对象已经完全构建,构造函数除了返回之外别无他法。我搜索了其他问题以寻找该问题的更具体答案,并且也查看了引用资料,但没有找到任何说“你不应该因为这样那样的不良行为”,只有说“你不应该。”
在构造函数中启动线程在概念上与这种情况有何不同:
class SomeClass
{
public ImportantData data = null;
public SomeClass(ImportantData d)
{
// OtherClass.someExternalOperation(this); // Not a good idea
data = d;
OtherClass.someExternalOperation(this); // Usually accepted as OK
}
}
另一方面,如果类(class)是最终的呢?
final class SomeClass // like this
{
...
我看到很多关于此的问题和不应该回答的问题,但没有人提供解释,所以我想我应该尝试添加一个包含更多详细信息的问题。
最佳答案
But the object has been fully constructed, the constructor has nothing left to do but return
是也不是。问题在于,根据 Java 内存模型,编译器能够重新排序构造函数操作,并在构造函数完成后实际完成对象的构造函数。 volatile
或 final
字段将保证在构造函数完成之前初始化,但不保证(例如)您的 ImportantData data
字段将在构造函数完成时正确初始化。
但是正如@meriton 在评论中指出的那样,在与线程和启动它的线程的关系之前发生了关系。在#2 的情况下,你没问题,因为 data
必须在线程启动之前完全分配。这是根据 Java 内存模型保证的。
也就是说,将其构造函数中的对象引用“泄漏”到另一个线程被认为是不好的做法,因为如果在 t.start() 之后添加了任何构造函数行
如果线程看到对象是否完全构造,这将是一个竞争条件。
这里还有一些阅读:
- 这是一个值得阅读的好问题:calling thread.start() within its own constructor
- Doug Lea's memory model page谈论指令重新排序和构造函数。
- 这是一篇关于 safe constructor practices 的精彩文章其中更多地讨论了这一点。
- 这就是 "double check locking" problem 出现问题的原因
- 我对这个问题的回答是相关的:Is this a safe publication of object?
关于java - 为什么我不应该在类的构造函数中使用 Thread.start()?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11834173/