web-dev-qa-db-ja.com

クラスがスレッドプールに公開されたときにThreadLocalリソースをクリーンアップするのは本当に私の仕事ですか?

ThreadLocalの使用

私のJavaクラスでは、主に不要なオブジェクトの作成を回避する手段として、ThreadLocalを時々使用します:

_@net.jcip.annotations.ThreadSafe
public class DateSensitiveThing {

    private final Date then;

    public DateSensitiveThing(Date then) {
        this.then = then;
    }

    private static final ThreadLocal<Calendar> threadCal = new ThreadLocal<Calendar>()   {
        @Override
        protected Calendar initialValue() {
            return new GregorianCalendar();
        }
    };

    public Date doCalc(int n) {
        Calendar c = threadCal.get();
        c.setTime(this.then):
        // use n to mutate c
        return c.getTime();
    }
}
_

私はこれを適切な理由で行います-GregorianCalendarは、見事にステートフルで変更可能な、スレッドセーフではないオブジェクトの1つであり、値を表すのではなく、複数の呼び出しにわたってサービスを提供します。さらに、インスタンス化は「高価」であると見なされます(これが正しいかどうかは、この質問の要点ではありません)。 (全体的に、私は本当にそれを賞賛します:-))

Tomcatの鳴き声

ただし、スレッドをプールする任意の環境でこのようなクラスを使用すると、アプリケーションがそれらのスレッドのライフサイクルを制御していないandの場合、メモリリークが発生する可能性があります。サーブレット環境が良い例です。

実際、Tomcat 7はWebアプリケーションが停止するとそのように鳴ります。

重大:Webアプリケーション[]が、タイプ[org.Apache.xmlbeans.impl.store.CharUtil $ 1](値[[email protected]])のキーと値を持つThreadLocalを作成しましたタイプ[Java.lang.ref.SoftReference](値[[email protected]])ですが、Webアプリケーションが停止しているときに削除できませんでした。スレッドは、可能性のあるメモリリークを回避するために、時間の経過とともに更新されます。 2012年12月13日12:54:30 PM org.Apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(その特定のケースでは、私のコードでさえそうしていません)。

誰が悪いのか?

これは公平に思えません。 Tomcatは、正しいことを行ったとしてme(または私のクラスのユーザー)を非難しています。

結局のところ、Tomcatはother Webアプリのために、私に提供したスレッドを再利用したいと考えているからです。 (うーん、汚い気がします。)おそらく、Tomcatの側では、それは素晴らしいポリシーではありません-スレッドは実際に状態を持っている/原因があるので、アプリケーション間で共有しないでください。

ただし、このポリシーは望ましくない場合でも、少なくとも一般的です。 ThreadLocalユーザーとして、自分のクラスがさまざまなスレッドにアタッチしたリソースを「解放」する方法を提供する義務があると感じています。

しかし、それについて何をすべきか?

ここで正しいことは何ですか?

私には、サーブレットエンジンのスレッド再利用ポリシーがThreadLocalの背後にある意図と矛盾しているようです。

しかし、おそらく、ユーザーが「このクラスに関連付けられた、悪質なスレッド固有の状態になりましたが、スレッドを終了させて​​GCに任せる立場にないのですが」とユーザーに言わせる機能を提供する必要があります。私がこれを行うことさえ可能ですか?つまり、過去にThreadLocal#remove()を見た各スレッドでThreadLocal#initialValue()が呼び出されるようにすることはできません。それとも別の方法がありますか?

それとも、ユーザーに「適切なクラスローダーとスレッドプールの実装を取得する」とだけ言うべきでしょうか。

EDIT#1:スレッドのライフサイクルを認識しないバナイラユーティリティクラスでのthreadCalの使用方法を明確化EDIT#2DateSensitiveThingのスレッドセーフティの問題を修正

44
David Bullock

ため息、これは古いニュースです

さて、これでパーティーに少し遅れました。 2007年10月、Josh Bloch(Doug Leaと共に_Java.lang.ThreadLocal_の共著者) 書き込み

「スレッドプールの使用には細心の注意が必要です。スレッドローカルのずさんな使用と組み合わせたスレッドプールのずさんな使用は、多くの場所で指摘されているように、意図しないオブジェクト保持を引き起こす可能性があります。」

それでも、人々はThreadLocalとスレッドプールの不適切な相互作用について不満を言っていました。しかし、ジョシュは制裁を行いました:

「パフォーマンスのためのスレッドごとのインスタンス。AaronのSimpleDateFormatの例(上記)は、このパターンの一例です。」

いくつかのレッスン

  1. 任意の種類のオブジェクトをオブジェクトプールに配置する場合は、それらを「後で」削除する方法を提供する必要があります。
  2. ThreadLocalを使用して「プール」する場合、そのためのオプションは限られています。いずれか:a)あなたが知っている値を入れるThread(s)は、アプリケーションが終了すると終了することを知っています。 OR b)後で調整して、ThreadLocal#set()を呼び出した同じスレッドを呼び出して、ThreadLocal#remove( )アプリケーションが終了したとき
  3. そのため、オブジェクトプールとしてThreadLocalを使用すると、アプリケーションとクラスの設計にかなりの負担がかかります。メリットは無料ではありません。
  4. したがって、Joshua Blochが「効果的なJava」でそれを検討するように促したとしても、ThreadLocalの使用はおそらく時期尚早の最適化です。

つまり、「ローカルスレッドインスタンスプール」への高速で競合のないアクセスの形式としてThreadLocalを使用することを決定することは、軽視すべき決定ではありません。

注:「オブジェクトプール」以外のThreadLocalの使用法は他にもあり、これらのレッスンは、ThreadLocalが一時的にのみ設定されることを意図している場合や、維持するスレッドごとの状態が本物である場合には適用されません。トラックの。

ライブラリの実装者への影響

ライブラリの実装者にとっては、いくつかの影響があります(そのようなライブラリがプロジェクトの単純なユーティリティクラスである場合でも)。

どちらか:

  1. ThreadLocalを使用して、長時間実行されているスレッドを追加の手荷物で「汚染」する可能性があることを完全に認識します。 _Java.util.concurrent.ThreadLocalRandom_を実装している場合は、適切な場合があります。 (_Java.*_で実装していない場合、Tomcatは引き続きライブラリのユーザーに怒鳴る可能性があります)。 _Java.*_がThreadLocalテクニックを節約するための規律に注目するのは興味深いことです。

OR

  1. ThreadLocalを使用し、クラス/パッケージのクライアントに次のことを行います。a)最適化を無視することを選択する機会( "ThreadLocalを使用しないでください...クリーンアップを手配できません"); AND b)ThreadLocalリソースをクリーンアップする方法(「ThreadLocalを使用しても問題ありません... LibClass.releaseThreadLocalsForThread()の呼び出しに使用したすべてのスレッドを、それらが終了したときに調整できます。

ただし、ライブラリを「適切に使用するのが難しい」ようにします。

OR

  1. クライアントに、独自のオブジェクトプール実装を提供する機会を提供します(ThreadLocalまたは何らかの同期を使用する場合があります)。 (「OK、本当に必要な場合はnew ExpensiveObjectFactory<T>() { public T get() {...} }を提供できます。」.

そんなに悪くない。オブジェクトが本当に重要で作成に費用がかかる場合、明示的なプーリングはおそらく価値があります。

OR

  1. とにかく、それはアプリにとってそれほど価値がないと判断し、問題に取り組む別の方法を見つけます。作成に費用がかかり、変更可能で、スレッドセーフではないオブジェクトが原因で問題が発生します...とにかく、それらを使用するのが本当に最良のオプションですか

代替案

  1. 通常のオブジェクトプーリングと、競合するすべての同期。
  2. オブジェクトをプールしない-ローカルスコープでインスタンス化し、後で破棄するだけです。
  3. スレッドをプールしない(必要に応じてクリーンアップコードをスケジュールできる場合を除く)-JaveEEコンテナーで自分のものを使用しないでください
  4. 何も気にせずにThreadLocalをクリーンアップできるほどスマートなスレッドプール。
  5. 「アプリケーションごと」にスレッドを割り当て、アプリケーションの停止時にスレッドを停止させるスレッドプール。
  6. 「アプリケーションシャットダウンハンドラー」の登録を許可するスレッドプーリングコンテナーとアプリケーションの間のプロトコル。コンテナーは、アプリケーションのサービスに使用されたスレッドで実行するようにスケジュールできます。そのスレッドが将来のある時点で次に利用できる。例えば。 servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

将来のJavaEE仕様で、最後の3つの項目に関するいくつかの標準化が見られるといいですね。

ブートノート

実際、GregorianCalendarのインスタンス化はかなり軽量です。ほとんどの作業が発生するのはsetTime()への避けられない呼び出しです。また、スレッドの実行の異なるポイント間で重要な状態を保持しません。 CalendarThreadLocalに入れても、プロファイリングでnew GregorianCalendar()にホットスポットが確実に表示されない限り、コストよりも多くの効果が得られる可能性はほとんどありません。

new SimpleDateFormat(String)は、フォーマット文字列を解析する必要があるため、比較するとコストがかかります。解析されると、オブジェクトの「状態」は、後で同じスレッドが使用するときに重要になります。それはより良い適合です。ただし、クラスに追加の責任を与えるよりも、新しいインスタンスをインスタンス化する方が「費用がかからない」場合があります。

33
David Bullock

スレッドはあなたが作成したものではなく、あなたが借りただけです。使用する前に、きれいにしておかなければならないのは当然のことだと思います。 Tomcatはそれ自体ですべてをクリーンアップできますが、忘れ物を思い出させてくれます。

追加:準備されたGregorianCalendarの使用方法は単純に間違っています。サービスリクエストは同時に実行でき、同期がないため、doCalcは別のリクエストによってgetTime ater setTimeを呼び出すことができます。同期を導入すると処理が遅くなるため、新しいGregorianCalendarを作成する方が良いオプションになる可能性があります。

言い換えると、質問は次のとおりです。準備されたGregorianCalendarインスタンスのプールを保持して、その数が要求レートに合わせて調整されるようにする方法。したがって、少なくとも、そのプールを含むシングルトンが必要です。各Iocコンテナーには、シングルトンを管理する手段があり、ほとんどのオブジェクトには実装可能なオブジェクトプールがあります。 IoCコンテナをまだ使用していない場合は、ホイールを再発明するのではなく、IoCコンテナ(String、Guice)を使い始めます。

4

その助けがあれば、カスタムSPI(インターフェース)とJDK ServiceLoaderを使用します。次に、スレッドローカルのアンロードを実行する必要があるすべてのさまざまな内部ライブラリ(jar)をServiceLoaderパターンに従うので、jarがスレッドローカルクリーンアップを必要とする場合、適切な/META-INF/services/interface.nameがあれば、jarは自動的に選択されます。

次に、フィルターまたはリスナーでアンロードを実行します(リスナーにいくつかの問題がありましたが、何を思い出すことができません)。

JDK/JEEにスレッドローカルをクリアするための標準 SPI=)が付属していると理想的です。

1
Adam Gent

JDKのThreadPoolExecutorは、タスクの実行後にThreadLocalsのクリーニングを実行できたと思いますが、そうではありません。私はそれが少なくともオプションを提供できたと思います。スレッドがTreadLocalマップへのパッケージプライベートアクセスのみを提供するため、ThreadPoolExecutorは、スレッドのAPIを変更せずにそれらにアクセスできないためです。

興味深いことに、ThreadPoolExecutorはメソッドスタブbeforeExecutionおよびafterExecutionを保護しています。APIによると:These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals...。したがって、ThreadLocalCleanerインターフェースを実装するTaskと、afterExecutionでtaskのcleanThreadLocals()を呼び出すカスタムThreadPoolExecutorを想像できます。

0

このことを1年間考えた後、JavaEEコンテナがプールされたワーカースレッドを無関係なアプリケーションのインスタンス間で共有することは受け入れられないと判断しました。これは「エンタープライズ」ではありません。

実際にスレッドを共有する場合は、_Java.lang.Thread_(少なくともJavaEE環境では)がsetContextState(int key)forgetContextState(int key)(mirroring setClasLoaderContext())は、さまざまなアプリケーション間でスレッドを渡すときに、コンテナがアプリケーション固有のThreadLocal状態を分離できるようにします。

_Java.lang_名前空間でのこのような変更は保留中ですが、アプリケーションデプロイヤが「1つのスレッドプール、1つの関連アプリケーションのインスタンス」ルールを採用すること、およびアプリケーション開発者が「このスレッドはThreadDeathまでマイニングすること」私たちは一部を行います。

0
David Bullock