本文へ移動

CooLWJを検索

选择搜索范围,输入关键词后按回车。

言語

Javaスレッドコンテキスト

ThreadLocalはJDKのjava.langパッケージにある、よく使われるユーティリティクラスで、スレッドコンテキストの機能を実現しています。最も一般的な用途は、ログイン後のユーザーコンテキストなどの重要な情報をThreadLocalに保存することでしょう。

スレッドコンテキストは、一見複雑に見える多くの問題もすっきりと解決できます。例えば、springとmybatisのフレームワーク統合の問題です:
springのエコシステムを使って開発するとき、通常はトランザクション管理をspringに委ね、transactionalアノテーションでトランザクションの開始や分離レベルなどを設定します。データベースの操作もjdbcを直接使うのではなく、mybatisフレームワーク経由で行います。springとmybatisは完全に独立した二つのフレームワークですが、使っていると、springが開いたデータベースconnectionをmybatisがそのまま取得して利用できるように見えます。こちらはほとんど何もする必要がなく、mybatis-spring-boot-staterのjarを一つ追加するだけで済みます。
この洗練された設計を実現する鍵はThreadLocalです。大まかな仕組みとしては、stater jarパッケージが最終的に次のものを導入してくれますMybatisAutoConfigurationそして SPI を通じてサービスの bean コンテナーに注入し、MybatisAutoConfiguration内で設定済みですmybatis必要なSqlSessionFactory、さらにspring独自のものを注入しSpringManagedTransactionFactory(実装したTransactionFactory、トランザクションファクトリーはmybatisが提供する拡張機構で、トランザクション管理を外部に委譲できる)。
全体の実行の流れとしては、transactionalアノテーションを付けたメソッドは最終的にAOPプロキシの対象になります。プロキシクラスは前処理のアドバイスでdataSourceからconnectionを取得し、最終的にThreadLocalに格納します(実際にThreadLocalに格納されるのはmapで、keyがdataSource、valueがconnectionに相当するconnectionHolderです)。その後、mybatisがsqlを実行するときは、まず次を介してTransactionFactoryconnectionを取得する。この時点で注入されているTransactionFactoryはいSpringManagedTransactionFactory,そのためThreadLocalから対応するdataSourceのconnectionを取り出す。

ThreadLocalの基本原理の紹介:

tapd_50394669_1660562598_27.png

ThreadLocalMap線形探索法で衝突を解決するハッシュテーブルであり、スレッドごとに一つ生成され、各ThreadLocalオブジェクトには一つのthreadLocalHashCodeハッシュキーの計算に使う。
HashMapと比較してみると、ThreadLocalMap衝突解決アルゴリズムとしてチェイン法ではなく線形探索法を選んだのは、以下の点を考慮したためかもしれません:
1、メモリ面の考慮です。用途が異なるため、ThreadLocalMapのデータ量は一般にHashMapよりはるかに少なく、データ量が少ない場合、チェイン法ではポインタによるメモリ消費が大きく増えます
2、効率。1を踏まえ、要素数が少ない場合に節約できた領域を使って負荷率の低いハッシュテーブルを生成することで、衝突を減らし、平均検索速度を向上させられます

ThreadLocal、スレッド、ThreadLocalMapの関係図

問題
A スレッドプールを使うタスクの場面では、コンテキストの受け渡しに問題がある。特にTomcatベースのWebプログラミングモデルでは、Tomcat自体がスレッドプールを使ってサーバーとクライアント間のリクエストとレスポンスのやり取りを処理するため、各Webアプリケーションはスレッドコンテキストを正しく受け渡す処理を手動で行わなければならない。
B 効率の問題。アプリケーション内の ThreadLocal が多すぎてハッシュテーブルが肥大化すると、検索速度が低下する

問題Aについては、一般的な解決方法はほぼ共通しているので、私たちのシステムのコードをそのまま掲載します
2.png
この方式には二つの問題があります:
1、後から追加されたThreadLocalを正しく伝播させる必要がある場合、コードを修正し、wrapの処理に新しいThreadLocalを追加する必要があり、漏れがあるとバグが発生します
2、状況によっては、タスクを投入したスレッド自身がそのタスクを実行することがあります。例えば:

  • スレッドプールが満杯になり、かつスレッドプールの RejectedExecutionHandler 使用しているのは CallerRunsPolicy の場合、スレッドプールに投入されたタスクは投入元のスレッドで直接実行されるため、最後のクリーンアップ処理によって投入元スレッドのコンテキストが失われます

  • 同様に、使用するのは ForkJoinPool (並列実行を含む Stream と CompletableFuture 、内部では ForkJoinPool )の場面で、展開された ForkJoinTask はタスクを投入したスレッドで直接実行される。最後のクリーンアップによっても、投入元スレッドのコンテキストが失われる

解決策:
問題1:ThreadLocal クラスに、現在アプリケーション内に ThreadLocal オブジェクトが全部でいくつあるかを取得できる API があれば、すぐに解決するのですが、残念ながら対応していません。無理に実現するなら、リフレクションを使って thread オブジェクト内のinheritableThreadLocals(つまり一つのThreadLocalMap)プロパティを取得し、ハッシュテーブルを走査してすべてのentryを取り出せば、すべてのthreadlocalのキーと値のペアを取得できます。もう一つの方法は、jdkのThreadLocalをさらに一層ラップして、上記の機能を追加することです。
問題2:wrap のロジックを改善することで解決できます:スレッドの実行開始時に、まず現在のスレッドのコンテキストを取得して一時保存し、次にタスクを投入したスレッドのコンテキストでこのスレッドのコンテキストを置き換えます。スレッドの実行終了前には消去せず、代わりに一時保存したデータでこのスレッドのコンテキストを復元します。

transmittable-thread-local
使える仕組みはすでにあり、その実装原理は上記の二つの方向から取り組むというものだ。そして、この処理の流れには名前も付けられている CRR(capture/replay/restore) パターン。
使い方は三つあります:
1 ### 修飾 Runnable と Callable

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent"); 
Runnable task = new RunnableTask(); 
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);  
// =====================================================  
// Task中可以读取,值是"value-set-in-parent" 
String value = context.get();

2 スレッドプールのラップ

ExecutorService executorService = ... 
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);  
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent");  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// =====================================================  
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

3 ### 使用 Java Agent で修飾する JDK スレッドプールの実装クラス

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## 
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); 
context.set("value-set-in-parent");  
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## 
ExecutorService executorService = Executors.newFixedThreadPool(3);  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// ## 3. 框架下层逻辑 ## 
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

java agent方式を使う利点は、アプリケーションコードに手を加えずに済むことです。欠点として私が最も問題だと思うのは、JVMの起動時に引数を付け忘れると、障害につながる可能性があることです。

ソースコードのロジックを簡単に見てみよう
3.png

4.png 

問題Bについては、実際のアプリケーション開発では時間効率に対する要求がそれほど高くないため、一般には考慮しません。しかし、低レベルのミドルウェア開発では、性能の最適化が重視される場合があります。例えばnettyは、自ら実装することを選びました。その対象はFastThreadLocaljdkのThreadLocalを置き換え、同時に対応するFastThreadLocalThread、FastThreadLocalThreadとFastThreadLocal組み合わせて使用すると、常にO(1)の検索時間計算量を保証できます。この解決策の原理は、空間を使って時間を節約するというものです。ハッシュテーブルでハッシュ衝突が発生した後に検索効率が低下するのを避けるため、十分に長い配列を生成し、各FastThreadLocal構築時にはそれぞれに一意の配列インデックスが割り当てられます。1人につき専用の枠が一つあるようなもので、検索時には自分のindexを使って配列から直接取り出すだけで済みます。
ソースコードを簡単に載せておく
5.png


コメント

送信中

好きなテーマを選ぶ

選んだテーマはサイト全体に適用されます。