Tomcat NIOコネクタでThreadLocalを安全に使用できるかどうか

これはロードテスト中にTomcat NIOコネクタをテストするときに気になりました。私はさらにThreadLocalを使用しています。私はSpringを使用しています。私はいくつかの場所でそれを利用しています。

NIOコネクタは接続ごとにスレッドを持っていないので、ThreadLocalオブジェクトがクリーンアップされる前に別のスレッドと共有されていれば、バグを発見するのが非常に困難になることがあります。しかし、これは問題ではないと私は見つけることができた文書化された警告ではないと私はこれについて警告する他の投稿を見つけたことはありません。私はNIOコネクタが実際の要求に対応するスレッドに影響を与えないと仮定します。

私がこの前提に着く前に、私はある具体的な証拠を見つけることを望んでいました。

8

3 答え

Tomcatのコードに精通した人だけが具体的な答えを与えることができますが、私は木製のものを試してみます:)

まず、単にNIOコネクタを使用するかどうか、またはAsyncサーブレットについても話しているかどうかを明確にする必要があります。 答えはそれぞれの場合で若干異なります。

注意すべき主な点は、Javaには継続、コルーチン、スレッドの再スケジューリングが一切ないということです。つまり、スレッドで実行されているコードを起動すると、完了するまで そのコードがスレッド上で実行されます。

つまり、 doSomething が実行されている間は、 myObject.doSomething(); を実行すると、そのスレッドに排他的なアクセス権が与えられます。スレッドは、使用しているIOモデルの種類にかかわらず、他のコードに切り替えることはありません。

何が起きるかは、異なるスレッドが異なるCPU上で実行されるようにスケジューリングされるが、各スレッドは1つのコードを完了して実行することである。

したがって、 doSomething が次の場合:

public static final ThreadLocal VALUE = new ThreadLocal();
public void doSomething() {
  VALUE.set(this);
  try {
    doSomethingElse();
  } finally {
    VALUE.set(null);
  }
}

doSomethingElse は1つのスレッドを実行し、スレッドローカルは実行全体に対して正しい値に設定されます。

したがって、単純なNIOコネクタでは違いはありません。コンテナはサーブレットの service メソッドを呼び出し、サーブレットは単一スレッドで実行され、最後にすべて完了します。コンテナがIOをより効率的に処理できるのは、接続を処理することだけです。

非同期サーブレットを使用している場合は、少し異なります。その場合、(非同期モデルの動作方法のため)サーブレットが1回のリクエストに対して複数回呼び出され、それらの呼び出しが異なるスレッド上にある可能性があります。あなたのサーブレットの呼び出しの間に何かをスレッドローカルに格納しないでください。しかし、あなたのサービスメソッドへの一回の呼び出しのために、それはまだ素晴らしいです。

HTH。

7
追加された
それは、ThreadLocalは非同期のコードを持たないサーブレットに対してのみ適格であり、非同期を利用するサーブレットには適さないということですか?
追加された 著者 pacman,

これを確認するには、依然としてリクエストを処理する1つのスレッドです Tomcatメーリングリストのここ

3
追加された

PammanからのTimとフォローアップの質問から受け入れられた回答に追加するには、AsyncResponseまたは同様の機能をNIOコネクタとともに使用するときに注意する必要があります。私は、Timが何を意味するのか分かりません。 "あなたの[async]サーブレットは、単一の要求に対して複数回呼び出されるかもしれません" ...しかし、 "要求"が単一の "GET"、 "PUT"、 "POST" 、 "DELETE"、そしてAFAIKを実行すると、サーブレット内の対応するリソースメソッドを1回呼び出すことになります。

ThreadLocalsとAsyncリソースで実行できる問題の1つは、非同期リソース内の処理スレッドがNIOイベントループThreadのThreadLocal変数のコピーを必要とする場合です。言い換えれば、NIOイベントループのスレッドは要求を受け取り、次に非同期リソースに制御を渡します...そのリソースは子スレッドに制御を渡します。そして、NIOイベントループスレッドは別の要求を自由に処理できます... so NIOイベントループThread 内の任意のThreadLocal変数は、後続の要求によってストンプされる可能性があります。

新しいリクエストごとに、ThreadLocalに格納されたObjectの新しいインスタンスを作成することもできます。この場合、新しいリクエストごとに、以前のリクエスト中に同じThreadLocalに格納されていた古いインスタンスが詰まることはありません。あなたが扱っているケースを確認する必要があります...いくつかの例を見てみましょう。

元の質問はSpringを指すので、良い例はThreadLocalを持つRequestContextHolderです。 NIOイベントループスレッドの名前が「http-nio-8080-exec-1」で、制御がAsyncResponseリソースに渡され、Executorを介して新しいスレッド(「pool-2-thread-3」という名前の) 。新しいスレッドには、RequestAttributesから何かを必要とするコードがあり、AsyncResponse.resume()を介して返答を返すようになっています。スレッド "pool-2-thread-3"で実行されているコードは "http-nio-8080-exec-1"からRequestAttributesにアクセスする必要があるので、2つのことを確認する必要があります。

1)あなたのリソースは "http-nio-8080-exec-1"からRequestAttributesへの参照を取得し、それを "pool-2-thread-3"に渡します。

2) "http-nio-8080-exec-1"が新しいリクエストを受け入れると、RequestAttributesの新しいコピーを作成し、それを新しいリクエストのRequestContextHolderのThreadLocalコピーにセットします(Springコードはこのように動作します。安全です)。

逆の例は、マップのlog4j MDC ThreadLocalコピーです。この場合、新しいリクエストごとに同じMapが再利用されるため、NIOイベントループのスレッドからAsyncResponseスレッドへマップの参照を渡すことは安全ではありません。マップのコピーを作成して渡す必要があります。 MDCAwareThreadPoolExectutor を参照してください。

基本的には、NIOイベントループのスレッドからAsyncResponseスレッドに渡す必要がある各ThreadLocal変数をチェックして、元のObjectへの参照を渡すだけで安全かどうかを確認する必要があります。ワーカースレッドのThreadLocal変数にコピーを設定する前に、オブジェクトのコピーを作成します。

ところで、上記の2つの例を組み合わせたコードがあります:

public class RequestContextAwareThreadPoolExecutor extends MDCAwareThreadPoolExecutor {
    /* ... constructors left out ... */

    @Override
    public void execute(Runnable runnable) {
        super.execute(wrap(runnable, RequestContextHolder.currentRequestAttributes()));
    }

    Runnable wrap(final Runnable runnable, final RequestAttributes requestAttributes) {
        return() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            try {
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

AsyncResponseリソースから、次のような呼び出しを行います。

executor.execute(() -> {
   //veryLongOperation() needs to access the RequestAttributes and the MDC
    asyncResponse.resume(veryLongOperation());
});
1
追加された