4

クラスのコンストラクター内でスレッドの開始メソッドを呼び出すべきではない理由について、私は正当性を探してきました。次のコードを検討してください。

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 メモリー・モデルに従って、コンパイラーがコンストラクター操作の順序を変更し、コンストラクターの終了にオブジェクトのコンストラクターを実際に終了できることです。 volatileまたはfinalフィールド、コンストラクターが終了する前に初期化されることが保証されますが、(たとえば)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
}

Database はある種のデータベース抽象化インターフェースであり、MyEvents はデータベースへの変更を通知する必要があるものによって生成され、DBJanitor は MyEvents を Database に適用する方法を知っている Thread のサブクラスです。ご覧のとおり、この実装では、構成された OracleDatabase クラスを Database 実装として使用します。

これで問題ありませんが、プロジェクトの要件が変更されました。新しいプラグインは、既存のコードベースを使用できる必要がありますが、Microsoft Access データベースにも接続できる必要があります。これをサブクラスで解決することにします。

class AccessDBEventManager extends DBEventManager()
{
    public AccessDBEventManager(String filename)
    {
        super();
        db = new MSAccessDatabase(filename);
    }
}

しかし、見よ、コンストラクターでスレッドを開始するという私たちの決定は、今私たちを悩ませるために戻ってきています。クライアントの安っぽい 700MHz シングル コア pentium II で実行すると、このコードには競合状態が発生します。数回の起動ごとに 1 回、データベース マネージャを作成すると、データベースが作成されてスレッドが開始されるときに、「Hello Database!」が送信されます。イベントを間違ったデータベースに送信します。

これは、スレッドがスーパークラス コンストラクターの最後で開始されるために発生します... しかし、それは構築の終わりではありません。スーパークラスのメンバーの一部をオーバーライドするサブクラス コンストラクターによってまだ初期化されているため、スレッドがジャンプするときイベントをデータベースにディスパッチする際に、サブクラスのコンストラクターがデータベース参照を正しいデータベースに更新する前に、時々入ります。

これには少なくとも 2 つの解決策があります。

  1. クラスを final にすることができます。これにより、サブクラス化を防ぐことができます。これを行うと、他のオブジェクトに公開する前にオブジェクトが完全に構築されていることを確実に確認できます (まだコンストラクターを離れていなくても)。したがって、このような奇妙な動作が発生しないことが確実になります。

    コンストラクターでの割り当ての並べ替えを防ぐための手順も実行する必要があります。スレッドがアクセスするフィールドを揮発性として宣言するか、任意の種類の同期ブロックでそれらをラップすることができます。これら 2 つのオプションはそれぞれ、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 Access を作成できます。DBEventManager(new MSAccessDatabase("somefile.db"));

  2. あなたはそれを行うことができず、別の開始メソッドとオプションの静的ファクトリメソッドまたはコンストラクターを呼び出してから開始メソッドを呼び出すメソッドを使用する、それ以外の場合は一般的に受け入れられている方法にフォールバックします。

    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 に答える