web-dev-qa-db-ja.com

ThreadLocalsをクリーンアップする方法

これを行う方法の例はありますか?ガベージコレクターによって処理されますか? Tomcat 6を使用しています。

55
Ricardo

Javadocはこう言います:

「各スレッドは、スレッドが生きていてThreadLocalインスタンスがアクセス可能である限り、スレッドローカル変数のコピーへの暗黙的な参照を保持します。スレッドがなくなると、スレッドローカルインスタンスのすべてのコピーはガベージコレクションの対象になります(これらのコピーへの他の参照が存在しない限り)。

アプリケーションまたは(リクエストスレッドについて話している場合)コンテナがスレッドプールを使用している場合、スレッドは停止しません。必要に応じて、自分でスレッドローカルを処理する必要があります。これを行う唯一のクリーンな方法は、ThreadLocal.remove()メソッドを呼び出すことです。

スレッドプール内のスレッドのスレッドローカルをクリーンアップする必要がある理由は2つあります。

  • メモリ(または仮想的にリソース)のリークを防ぐため、または
  • スレッドローカルを介した、ある要求から別の要求への情報の偶発的な漏洩を防ぐため。

スレッドローカルのメモリリークは、通常、バインドされたスレッドプールでは大きな問題にはなりません。スレッドローカルは最終的に上書きされる可能性が高いためです。つまり、スレッドが再利用されるとき。ただし、(ThreadLocal変数を使用してシングルトンインスタンスを保持する代わりに)新しいstaticインスタンスを何度も作成するというミスを犯した場合、スレッドローカル値は上書きされません。 、各スレッドのthreadlocalsマップに蓄積されます。これにより、重大なリークが発生する可能性があります。


WebアプリケーションのHTTPリクエストの処理中に作成/使用されるスレッドローカルについて話していると仮定すると、スレッドローカルリークを回避する1つの方法は、WebアプリケーションのServletRequestListenerServletContextを登録することですリスナーのrequestDestroyedメソッドを実装して、現在のスレッドのスレッドローカルをクリーンアップします。

このコンテキストでは、1つの要求から別の要求にinformationが漏れる可能性も考慮する必要があることに注意してください。

68
Stephen C

実際のスレッドローカル変数への参照がない場合に、現在のスレッドからすべてのスレッドローカル変数を削除するコードを次に示します。他のスレッドのスレッドローカル変数をクリーンアップするように一般化することもできます。

    private void cleanThreadLocals() {
        try {
            // Get a reference to the thread locals table of the current thread
            Thread thread = Thread.currentThread();
            Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            Object threadLocalTable = threadLocalsField.get(thread);

            // Get a reference to the array holding the thread local variables inside the
            // ThreadLocalMap of the current thread
            Class threadLocalMapClass = Class.forName("Java.lang.ThreadLocal$ThreadLocalMap");
            Field tableField = threadLocalMapClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object table = tableField.get(threadLocalTable);

            // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
            // is a reference to the actual ThreadLocal variable
            Field referentField = Reference.class.getDeclaredField("referent");
            referentField.setAccessible(true);

            for (int i=0; i < Array.getLength(table); i++) {
                // Each entry in the table array of ThreadLocalMap is an Entry object
                // representing the thread local reference and its value
                Object entry = Array.get(table, i);
                if (entry != null) {
                    // Get a reference to the thread local object and remove it from the table
                    ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
                    threadLocal.remove();
                }
            }
        } catch(Exception e) {
            // We will tolerate an exception here and just log it
            throw new IllegalStateException(e);
        }
    }
33
lyaffe

ThreadLocal値をクリーンアップする方法はありません。ただし、それらを最初に入れたスレッドから(またはスレッドがガベージコレクションされます-ワーカースレッドの場合ではありません)。これは、サーブレットリクエストが終了したとき(またはAsyncContextをサーブレット3の別のスレッドに転送する前)にThreadLocalをクリーンアップする必要があることを意味します。サーバーが再起動されていないときにWebアプリがアンデプロイされる状況では、メモリがリークします。

このようなクリーンアップを行うのに適した場所は ServletRequestListener.requestDestroyed() です。

Springを使用する場合、必要なすべての配線が既に配置されているので、それらをクリーンアップすることを心配することなく、リクエストスコープに単純に置くことができます(自動的に行われます)。

RequestContextHolder.getRequestAttributes().setAttribute("myAttr", myAttr, RequestAttributes.SCOPE_REQUEST);
. . .
RequestContextHolder.getRequestAttributes().getAttribute("myAttr", RequestAttributes.SCOPE_REQUEST);
17
rustyx

Javadocドキュメントを再度注意深く読んでください。

'各スレッドは、スレッドがアクティブで、ThreadLocalインスタンスがアクセス可能である限り、スレッドローカル変数のコピーへの暗黙的な参照を保持します。スレッドがなくなると、スレッドローカルインスタンスのすべてのコピーはガベージコレクションの対象になります(これらのコピーへの他の参照が存在しない限り)。 '

何もきれいにする必要はありません。リークが生き残るには「AND」条件があります。したがって、スレッドがアプリケーションに耐えるWebコンテナでも、webappクラスがアンロードされる限り(親クラスローダーにロードされた静的クラスの参照のみがこれを防止し、これはThreadLocalとは関係ありませんが、一般的な問題静的データを含む共有jar)AND条件の2番目のレッグはもう満たされないため、スレッドローカルコピーはガベージコレクションの対象となります。

実装がドキュメントを満たしている限り、スレッドローカルはメモリリークの原因にはなりません。

1
Philippe P.

JVMは、ThreadLocalオブジェクト内にあるすべての参照なしオブジェクトを自動的にクリーンアップします。

これらのオブジェクトをクリーンアップする別の方法(たとえば、これらのオブジェクトは、周囲に存在するすべてのスレッドセーフオブジェクトである可能性があります)は、基本的にそれを保持するObject Holderクラス内に配置することです。その中に存在します。ここでも、finalizeメソッドをいつ呼び出すかは、ガベージコレクターとそのポリシーに依存します。

コードサンプルは次のとおりです。

public class MyObjectHolder {

    private MyObject myObject;

    public MyObjectHolder(MyObject myObj) {
        myObject = myObj;
    }

    public MyObject getMyObject() {
        return myObject;
    }

    protected void finalize() throws Throwable {
        myObject.cleanItUp();
    }
}

public class SomeOtherClass {
    static ThreadLocal<MyObjectHolder> threadLocal = new ThreadLocal<MyObjectHolder>();
    .
    .
    .
}
0
Parasu

この質問に対する回答は古いものですが、貢献したいと思います。私は同じ問題(gson threadlocalがリクエストスレッドから削除されない)に悩まされていました。また、サーバーがメモリ不足になったときはいつでも快適に再起動することができました(これは非常に時間がかかります!!)。

Java devモードに設定されたWebアプリのコンテキスト(サーバーはコードの変更を検出するたびにバウンスするように設定され、デバッグモードで実行される可能性があるため) 、私はすぐにthreadlocalsが素晴らしく、時には苦痛になることを学びました。すべてのリクエストにthreadlocal Invocationを使用していました。Invocationの中で。 'フィルタをブロックし、'最終 'ブロック内でそれを破棄します。

私が観察したこと(今のところこれをバックアップするメトリックはありません)は、複数のファイルに変更を加え、サーバーが変更の間に絶えずバウンドしていると、イライラしてサーバーを再起動することです(Tomcatは正確には) IDEから。ほとんどの場合、「メモリ不足」の例外が発生します。

これを回避する方法は、ServletRequestListenerの実装をアプリに含めることで、問題はなくなりました。何が起こっていたのかと思いますが、リクエストの途中で、サーバーが数回バウンスする場合、スレッドローカルがクリアされていないため(gsonを含む)、スレッドローカルに関するこの警告と2つまたは3つの警告が表示されますサーバーがクラッシュします。 ServletResponseListenerで明示的にthreadlocalsを閉じると、gsonの問題はなくなりました。

これが理にかなっており、スレッドローカルの問題を克服する方法のアイデアを提供してくれることを願っています。常に使用箇所の近くで閉じてください。 ServletRequestListenerで、各スレッドローカルラッパーをテストし、まだ何らかのオブジェクトへの有効な参照がある場合は、その時点でそれを破棄します。

また、クラス内でスレッドローカルを静的変数としてラップすることを習慣にすることも指摘する必要があります。そうすれば、ServeltRequestListenerで破棄することで、同じクラスの他のインスタンスがぶらぶらすることを心配する必要がなくなります。

0
mainas

@lyaffeの答えはJava 6に最適です。この答えは、Java 8で利用可能なものを使用して解決するいくつかの問題があります。

@lyaffeの答えは、MethodHandleが利用可能になる前にJava 6について書かれていました。反射によるパフォーマンスのペナルティに苦しんでいます。以下のように使用する場合、MethodHandleは、フィールドおよびメソッドへのゼロオーバーヘッドアクセスを提供します。

@lyaffeの答えも_ThreadLocalMap.table_を明示的に通過し、バグが発生しやすいです。同じことを行うThreadLocalMap.expungeStaleEntries()メソッドが利用可能になりました。

以下のコードには、expungeStaleEntries()を呼び出すコストを最小限に抑えるための3つの初期化メソッドがあります。

_private static final MethodHandle        s_getThreadLocals     = initThreadLocals();
private static final MethodHandle        s_expungeStaleEntries = initExpungeStaleEntries();
private static final ThreadLocal<Object> s_threadLocals        = ThreadLocal.withInitial(() -> getThreadLocals());

public static void expungeThreadLocalMap()
{
   Object threadLocals;

   threadLocals = s_threadLocals.get();

   try
   {
      s_expungeStaleEntries.invoke(threadLocals);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }
}

private static Object getThreadLocals()
{
   ThreadLocal<Object> local;
   Object result;
   Thread thread;

   local = new ThreadLocal<>();

   local.set(local);   // Force ThreadLocal to initialize Thread.threadLocals

   thread = Thread.currentThread();

   try
   {
      result = s_getThreadLocals.invoke(thread);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }

   return(result);
}

private static MethodHandle initThreadLocals()
{
   MethodHandle result;
   Field field;

   try
   {
      field = Thread.class.getDeclaredField("threadLocals");

      field.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflectGetter(field);

      result = Preconditions.verifyNotNull(result, "result is null");
   }
   catch (NoSuchFieldException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}

private static MethodHandle initExpungeStaleEntries()
{
   MethodHandle result;
   Class<?> clazz;
   Method method;
   Object threadLocals;

   threadLocals = getThreadLocals();
   clazz        = threadLocals.getClass();

   try
   {
      method = clazz.getDeclaredMethod("expungeStaleEntries");

      method.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflect(method);
   }
   catch (NoSuchMethodException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}
_
0
Nathan