4

我一直在寻找为什么不应该在类的构造函数中调用线程的 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 实例。

脚节点:

  1. 我完全理解为什么这是不安全的。如果 MyOperationThread 在以下语句完成之前尝试访问 SomeClass.data (并且数据已初始化),我将得到一个我没有准备好的异常。或者也许我不会。你不能总是用线程来判断。在任何情况下,我都会为以后奇怪的、意想不到的行为做好准备。

  2. 我不明白为什么这样做是禁地。至此,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
{
    ...

我看到很多关于这个的问题和你不应该回答的问题,但没有一个提供解释,所以我想我会尝试添加一个包含更多细节的问题。

4

2 回答 2

8

但是对象已经完全构造好了,构造函数只好返回了

是和不是。问题在于,根据 Java 内存模型,编译器能够重新排序构造函数操作,并在构造函数完成实际完成对象的构造函数。 volatileorfinal字段保证在构造函数完成之前被初始化,但不能保证(例如)您的ImportantData data字段将在构造函数完成时正确初始化。

然而,正如@meriton 在评论中指出的那样,在与线程和启动它的线程的关系之前发生了。在#2的情况下,你很好,因为data必须在线程启动之前完全分配。这是根据 Java 内存模型保证的。

也就是说,将其构造函数中的对象的引用“泄漏”到另一个线程被认为是不好的做法,因为如果在之后添加了任何构造函数行,t.start()那么如果线程会看到对象是否完全构造,这将是一个竞争条件。

这里还有一些阅读:

于 2012-08-06T19:03:33.383 回答
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!” 事件到错误的数据库。

发生这种情况是因为线程在超类构造函数的末尾开始......但这不是构造的结束,我们仍然被子类构造函数初始化,它覆盖了一些超类的成员,所以当线程跳转时在向数据库分派事件时,它偶尔会在子类构造函数将数据库引用更新到正确的数据库之前进入。

至少有两种解决方案:

  1. 您可以将您的课程设为最终课程,这将防止对其进行子类化。如果你这样做,你可以确定你的对象在暴露给任何其他对象之前已经完全构造好了(即使它还没有离开构造函数),从而确定不会发生像这样的奇怪行为。

    您还必须采取措施防止在构造函数中重新排序分配:您可以将线程将访问的字段声明为 volatile,或者您可以将它们包装在任何类型的同步块中。这两个选项中的每一个都对 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 安全地使用 Microsoft AccessDBEventManager(new MSAccessDatabase("somefile.db"));

  2. 您可以不这样做,而是使用其他普遍接受的方法,即使用单独的 start 方法和可选的静态工厂方法或调用构造函数然后调用 start 方法的方法,如下所示:

    public start()
    {
        t.start();
    }

    public static DBEventManager getInstance(String hostname)
    {
        DBEventManager dbem = new DBEventManager(hostname);
        dbem.start();
        return DBEventManager;
    }

我很确定我是理智的,但第二个意见会很好。

于 2012-08-06T18:56:21.313 回答