48

私の ThreadLocal の使用

私の Java クラスでは、ThreadLocal主に不必要なオブジェクトの作成を避ける手段として aを使用することがあります。

@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 の鳴き声

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

実際、Web アプリケーションが停止すると、Tomcat 7 は次のように動作します。

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

(その特定のケースでは、私のコードでさえそれを行っていません)。

誰が悪いのか?

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

最終的には、Tomcat が私に提供したスレッドを他のWeb アプリで再利用したいからです。(うーん、私は汚れているように感じます。) おそらく、それは Tomcat 側の優れたポリシーではありません - スレッドには実際に状態がある/原因があるためです - アプリケーション間でそれらを共有しないでください。

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

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

ここで何をするのが正しいですか?

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

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

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

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

4

5 に答える 5

35

はあ、これは古いニュースです

さて、これでパーティーに少し遅れました。java.lang.ThreadLocal2007 年 10 月、Josh Bloch ( Doug Lea との共著) は次のように書いています。

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

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

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

いくつかのレッスン

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

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

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

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

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

また:

  1. ThreadLocal を使用し、実行時間の長いスレッドを余分な荷物で「汚染」する可能性があることを十分に認識しています。を実装java.util.concurrent.ThreadLocalRandomしている場合は、適切かもしれません。(Tomcat で実装していない場合、Tomcat はまだライブラリのユーザーに文句を言うかもしれませんjava.*)。java.*ThreadLocal 手法を控えめに使用する規律に注目するのは興味深いことです。

また

  1. ThreadLocal を使用し、クラス/パッケージのクライアントに次のことを行います。および b) ThreadLocal リソースをクリーンアップする方法 (「ThreadLocal を使用しても問題ありません...LibClass.releaseThreadLocalsForThread()終了時に呼び出すために使用したすべてのスレッドを手配できます。

ただし、ライブラリを「適切に使用するのが難しく」なります。

また

  1. クライアントに、独自のオブジェクトプールの実装 (ThreadLocal または何らかの同期を使用する可能性があります) を提供する機会を与えます。(「new ExpensiveObjectFactory<T>() { public T get() {...} }本当に必要だと思うなら、差し上げます」.

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

また

  1. いずれにせよ、アプリにとってそれほど価値がないと判断し、問題に取り組む別の方法を見つけます。作成に費用がかかり、変更可能で、スレッドセーフではないオブジェクトはあなたに苦痛を与えています... とにかくそれらを使用するのが本当に最善の選択肢ですか?

代替案

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

将来の JavaEE 仕様で、最後の 3 つの項目について何らかの標準化が見られるとよいでしょう。

ブートノート

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

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

于 2012-12-13T23:51:27.853 に答える
4

スレッドはあなたが作成したのではなく、レンタルしただけなので、使用をやめる前にクリーニングを要求するのが妥当だと思います。ちょうど、レンタカーの返却時にタンクを満タンにするのと同じです。Tomcat は自分ですべてをきれいにすることができますが、忘れ物を思い出させてくれます。

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

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

于 2012-12-13T05:00:16.387 に答える
1

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

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

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

于 2015-03-09T15:05:12.313 に答える
0

JDK の ThreadPoolExecutor は、タスクの実行後に ThreadLocals のクリーニングを行うことができると思いますが、そうではないことがわかっています。少なくともオプションを提供できたと思います。その理由は、Thread がその TreadLocal マップへのパッケージ プライベート アクセスのみを提供するため、ThreadPoolExecutor が Thread の API を変更せずにそれらにアクセスできないことが原因である可能性があります。

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

于 2012-12-13T10:42:38.197 に答える