我一直在寻找理由,为什么你不应该在类的构造函数中调用线程的start方法。请考虑以下代码:
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实例的线程的子类。
Footnodes:
我完全理解为什么这是不安全的。如果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
}
}
另外,如果课程是最终的怎么办?
final class SomeClass // like this
{
...
我看到很多问题询问这个和你不应该的答案,但没有提供解释,所以我想我会尝试添加一个有更多细节的答案。
答案 0 :(得分:8)
但是对象已经完全构造,构造函数除了返回
之外什么也没做
是和否。问题是,根据Java内存模型,编译器能够重新排序构造函数操作,并在构造函数完成后实际完成对象的构造函数。保证在构造函数完成之前初始化volatile
或final
个字段 ,但不保证(例如)您的ImportantData data
字段将被正确初始化到构造函数完成时。
然而正如@meriton在评论中指出的那样,在与线程和启动它的线程的关系之前会发生一些事情。在#2的情况下,你很好,因为在线程启动之前必须完全分配data
。这是根据Java内存模型保证的。
也就是说,将其构造函数中对象的引用“泄漏”到另一个线程被认为是不好的做法,因为如果在 t.start()
之后添加了任何构造函数行,那么它将是如果线程看到对象已完全构造,则为竞争条件。
以下是更多阅读材料:
答案 1 :(得分:3)
考虑以下情况。您有一个运行调度程序线程的类,该任务线程将任务排队到数据库,编码方式与以下类似:
class DBEventManager
{
private Thread t;
private Database db;
private LinkedBlockingQueue<MyEvent> eventqueue;
public DBEventManager()
{
this("127.0.0.1:31337");
}
public DBEventManager(String hostname)
{
db = new OracleDatabase(hostname);
t = new DBJanitor(this);
eventqueue = new LinkedBlockingQueue<MyEvent>();
eventqueue.put(new MyEvent("Hello Database!"));
t.start();
}
// getters for db and eventqueue
}
数据库是某种数据库抽象接口,MyEvents由需要发出数据库更改信号的任何东西生成,而DBJanitor是Thread的子类,知道如何将MyEvents应用于数据库。我们可以看到,这个实现使用组成的OracleDatabase类作为数据库实现。
这一切都很好,但现在你的项目要求已经改变了。您的新插件必须能够使用现有的代码库,但也必须能够连接到Microsoft Access数据库。您决定使用子类解决此问题:
class AccessDBEventManager extends DBEventManager()
{
public AccessDBEventManager(String filename)
{
super();
db = new MSAccessDatabase(filename);
}
}
但是,请注意,我们决定在构造函数中启动线程现在又回来困扰着我们。运行在客户端蹩脚的700MHz单核pentium II上,此代码现在有一个竞争条件:每隔几次创建一次,创建数据库管理器,因为它创建数据库并启动线程,发送“Hello Database” !” event 到错误的数据库。
这是因为线程在超类构造函数的末尾开始...但是这不是构造的结束,我们仍然被子类构造函数初始化,它会覆盖一些超类的成员,所以当线程在调度事件中向右跳转到数据库,它偶尔会在子类构造函数将数据库引用更新到正确的数据库之前进入。
你可以让你的课成为最终,这将阻止它的继承。如果你这样做,你可以确保确定你的对象在完全构造之前,然后将它暴露给任何其他对象(即使它还没有离开构造函数),从而确保确定这种奇怪的行为不会发生。
您还必须采取措施防止构造函数中的赋值重新排序:您可以声明线程将作为volatile访问的字段,或者您可以将它们包装在任何类型的synchronized块中。这两个选项中的每一个都对JIT编译器可以执行的重新排序应用了额外的限制,这可以确保在线程访问它们时正确分配字段。
在这种情况下,您可能会与您的老板争论,直到他让您对代码库进行更改,这将涉及将DBEventManager的构造函数更改为如下所示:
private Thread t; // no getter, doesn't need to be volatile
private volatile Database db;
private volatile LinkedBlockingQueue<MyEvent> eventqueue;
public DBEventManager()
{
this("127.0.0.1:31337");
}
public DBEventManager(String hostname)
{
this(new OracleDatabase(hostname));
}
public DBEventManager(Database newdb)
{
db = newdb;
t = new DBJanitor(this);
eventqueue = new LinkedBlockingQueue<MyEvent>();
eventqueue.put(new MyEvent("Hello Database!"));
t.start();
}
如果您在开发早期就已经预见到了这个问题,那么您可能已经添加了额外的构造函数。然后,您可以使用DBEventManager(new MSAccessDatabase("somefile.db"));
你可以不这样做,并且回到使用单独的start方法和可选的静态工厂方法或者调用构造函数然后启动方法的方法的普遍接受的方法,如下所示: / p>
public start()
{
t.start();
}
public static DBEventManager getInstance(String hostname)
{
DBEventManager dbem = new DBEventManager(hostname);
dbem.start();
return DBEventManager;
}
我很确定我是理智的,但第二种意见会很好。