トピック:スレッドローカルストレージモード:共有なし、害なし
スレッドクロージャは本質的に共有を回避するためのものです。ローカル変数によって共有を回避できることはすでにご存じですが、他に方法はありますか?はい、Java言語ThreadLocal
が提供するスレッドローカルストレージ()で実行できます。最初にThreadLocalの使用方法を見てみましょう。
ThreadLocalの使い方
次の静的クラスThreadIdは、各スレッドに一意のスレッドIDを割り当てます。スレッドがThreadIdのget()メソッドを前後に2回呼び出す場合、2つのget()メソッドの戻り値は同じです。ただし、2つのスレッドがそれぞれThreadIdのget()メソッドを呼び出す場合、2つのスレッドから見えるget()メソッドの戻り値は異なります。ThreadLocalを初めて使用する場合、同じスレッドでget()メソッドを呼び出した結果が同じであるのに、異なるスレッドでget()メソッドを呼び出した結果が異なるのはなぜか疑問に思われるかもしれません。
static class ThreadId {
static final AtomicLong nextId=new AtomicLong(0);
//定义ThreadLocal变量
static final ThreadLocal<Long> tl=ThreadLocal.withInitial(
()->nextId.getAndIncrement());
//此方法会为每个线程分配一个唯一的Id
static long get(){
return tl.get();
}
}
この奇妙な結果はThreadLocalの傑作になる可能性がありますが、ThreadLocalの動作原理を詳細に説明する前に、実際の作業で遭遇する可能性のある例を見て、ThreadLocalの理解を深めましょう。SimpleDateFormatはスレッドセーフではないことを知っているかもしれませんが、並行シナリオで使用する必要がある場合はどうすればよいですか?
実際、ThreadLocalでそれを解決する方法があります。次のサンプルコードは、ThreadLocalソリューションの特定の実装です。このコードは、前のThreadIdコードと非常によく似ています。同様に、SafeDateFormatのget()メソッドを呼び出す異なるスレッドは、異なるSimpleDateFormatを返しますSimpleDateFormatは異なるスレッドで共有されないため、オブジェクトインスタンスはローカル変数と同様にスレッドセーフです。
static class SafeDateFormat {
//定义ThreadLocal变量
static final ThreadLocal<DateFormat> tl=ThreadLocal.withInitial(
()-> new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss"));
static DateFormat get(){
return tl.get();
}
}
//不同线程执行下面代码
//返回的df是不同的
DateFormat df = SafeDateFormat.get();
上記の2つの例を通して、ThreadLocalの使用法とアプリケーションのシナリオを理解したと思いますが、以下では、ThreadLocalの動作原理について詳しく説明します。
ThreadLocalの仕組み
ThreadLocalの仕組みを説明する前に、自分で考えてみてください。ThreadLocalの機能を実装する場合、どのように設計しますか?ThreadLocalの目的は、異なるスレッドに異なる変数Vを持たせることです。最も直接的な方法は、キーがスレッドで、値が各スレッドが所有する変数Vであるマップを作成することです。あまりにも。次の概略図とサンプルコードを参照すると、理解することができます。
class MyThreadLocal<T> {
Map<Thread, T> locals = new ConcurrentHashMap<>();
//获取线程变量
T get() {
return locals.get(Thread.currentThread());
}
//设置线程变量
void set(T t) {
locals.put(Thread.currentThread(), t);
}
}
JavaのThreadLocalはこの方法で実装されていますか?今回の設計アイデアは、Javaの実装とは大きく異なります。というJava実装にはMapもありますがThreadLocalMap
、これはThreadLocalではなく、ThreadLocalMapを保持するThreadです。Threadクラスには、タイプがThreadLocalMapであるプライベート属性threadLocalsがあり、ThreadLocalMapのキーはThreadLocalです。以下の概略図と単純化されたJava実装コードを組み合わせて理解できます。
class Thread {
//内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal<T>{
public T get() {
//首先获取线程持有的
//ThreadLocalMap
ThreadLocalMap map =Thread.currentThread().threadLocals;
//在ThreadLocalMap中
//查找变量
Entry e = map.getEntry(this);
return e.value;
}
static class ThreadLocalMap{
//内部是数组而不是Map
Entry[] table;
//根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key){
//省略查找逻辑
}
//Entry定义
static class Entry extends WeakReference<ThreadLocal>{
Object value;
}
}
}
一見すると、Javaの設計と実装はMapの所有者とのみ異なります。この設計では、MapはThreadLocalに属していますが、Java実装ではThreadLocalMapはThreadに属しています。これら2つの方法のどちらがより合理的ですか?明らかに、Java実装の方が合理的です。Javaの実装では、ThreadLocalは単なるプロキシツールクラスであり、内部にスレッド関連データを保持しません。すべてのスレッド関連データはThreadに格納されます。この設計は理解しやすくなっています。データアフィニティの観点から、ThreadLocalMapはThreadに属している方が合理的です。
もちろん、より深い理由があります。つまり、メモリリークが発生するのは簡単ではありません。私たちの設計では、ThreadLocalが保持するMapはThreadオブジェクトへの参照を保持します。つまり、ThreadLocalオブジェクトが存在する限り、Map内のThreadオブジェクトはリサイクルされません。ThreadLocalのライフサイクルは、通常、threadよりも長いため、この設計スキームはメモリリークを引き起こしやすいです。Javaの実装では、ThreadはThreadLocalMapを保持し、ThreadLocalMap内のThreadLocalへの参照は依然として弱い参照(WeakReference)なので、Threadオブジェクトをリサイクルできる限り、ThreadLocalMapをリサイクルできます。このJavaの実装はより複雑に見えますが、より安全です。
JavaのThreadLocalの実装は十分に検討されていると考える必要がありますが、それでもプログラマがメモリリークを100%回避することはできません。たとえば、スレッドプールでThreadLocalを使用すると、注意しないとメモリリークが発生する可能性があります。
ThreadLocalとメモリリーク
スレッドプールでThreadLocalを使用するとメモリリークが発生するのはなぜですか?その理由は、スレッドプール内のスレッドの生存時間が長すぎて、プログラムと一緒に停止することが多いためです。これは、Threadによって保持されているThreadLocalMapがリサイクルされず、ThreadLocalMapのエントリがThreadLocalになることを意味しますこれは弱い参照(WeakReference)であるため、ThreadLocalが独自のライフサイクルを終了する限り、再利用できます。**ただし、エントリの値はエントリによって強く参照されています。**したがって、値のライフサイクルが終了しても、値を回復できず、メモリリークが発生します。
スレッドプールで、どのようにThreadLocalを正しく使用しますか?実際、これは非常に単純であり、JVMはValueへの強い参照を自動的に解放できないため、手動で解放できます。手動でリリースするにはどうすればよいですか?リソースを手動で解放するための武器にすぎないtry {} finally {}スキームをすぐに考えると推定されます。サンプルコードは以下の通りですので、スタディを参考にしていただけます。
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
//手动清理ThreadLocal
tl.remove();
}
});
InheritableThreadLocalと継承
ThreadLocalで作成されたスレッド変数は、子スレッドに継承できません。つまり、スレッド内でThreadLocalを介してスレッド変数Vを作成してから、スレッドが子スレッドを作成すると、子スレッドのThreadLocalを介して親スレッドのスレッド変数Vにアクセスできなくなります。
親スレッドのスレッド変数を継承するために子スレッドが必要な場合はどうなりますか?実際、それは非常に簡単です。Javaはこの機能をサポートするためにInheritableThreadLocalを提供しています。InheritableThreadLocalはThreadLocalのサブクラスであるため、使用方法はThreadLocalと同じであるため、ここでは紹介しません。
ただし、スレッドプールでInheritableThreadLocalを使用することはお勧めしません。スレッドローカルと同じ欠点があるため、メモリリークが発生する可能性があるだけでなく、より重要な理由は、スレッドプールでのスレッドの作成が動的であり、簡単であることです。継承関係が乱雑です。ビジネスロジックがInheritableThreadLocalに依存している場合、メモリリークよりも致命的であることが多いビジネスロジック計算エラーが発生する可能性があります。
まとめ
スレッドローカルストレージモデルは本質的に共有を回避するソリューションです。共有がないため、当然、同時実行性の問題はありません。並行シナリオでスレッドセーフでないツールクラスを使用する必要がある場合、最も簡単な解決策は共有を避けることです。共有を避けるには2つの方法があります。1つはこのツールクラスをローカル変数として使用する方法で、もう1つはスレッドローカルストレージモードです。これらの2つのスキームでは、ローカル変数スキームの短所は、高い同時実行性のシナリオでオブジェクトが頻繁に作成されることです。スレッドローカルストレージスキームでは、各スレッドはツールクラスのインスタンスを作成するだけでよいため、オブジェクトを頻繁に作成する問題はありません。
スレッドローカルストレージモードは同時実行性の問題を解決するための一般的なソリューションであるため、Java SDKは対応する実装であるThreadLocalも提供します。上記の分析により、Java SDKの実装が十分に検討されていることを理解できるはずですが、それでも完全ではありません。たとえば、スレッドプールでThreadLocalを使用すると、メモリリークが発生する可能性があるため、ThreadLocalを使用するには、エネルギーを供給する必要があります。 、十分注意してください。
実際の作業では、ThreadLocalを使用してコンテキスト情報を転送する多くのプラットフォームベースの技術ソリューションがあります。たとえば、SpringはThreadLocalを使用してトランザクション情報を転送します。
デモ
自己实现
/**
* 始终已当前线程作为Key值
*
* @param <T>
*/
public class ThreadLocalSimulator<T> {//模拟器
private final Map<Thread, T> storage = new HashMap<>();//非线程安全的集合类
public void set(T t) {
synchronized (this) {
Thread key = Thread.currentThread();
storage.put(key, t);
}
}
public T get() {
synchronized (this) {
Thread key = Thread.currentThread();
T value = storage.get(key);
if (value == null) {
return initialValue();
}
return value;
}
}
public T initialValue() {
return null;
}
}
public class ContextTest {
public static void main(String[] args) {
IntStream.range(1, 5)
.forEach(i ->
new Thread(new ExecutionTask()).start()
);
}
}
public class ExecutionTask implements Runnable {
private QueryFromDBAction queryAction = new QueryFromDBAction();
private QueryFromHttpAction httpAction = new QueryFromHttpAction();
@Override
public void run() {
queryAction.execute();//这一步的查询结果set到ThreadLocal中
System.out.println("The name query successful");
httpAction.execute();//取出set的结果做自己的业务
System.out.println("The card id query successful");
Context context = ActionContext.getActionContext().getContext();
System.out.println("The Name is " + context.getName() +
" and CardId " + context.getCardId());
}
}
public class QueryFromDBAction {
public void execute() {
try {
Thread.sleep(1000L);
String name = "Alex " + Thread.currentThread().getName();
ActionContext.getActionContext().getContext().setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class QueryFromHttpAction {
public void execute() {
Context context = ActionContext.getActionContext().getContext();
String name = context.getName();
String cardId = getCardId(name);
context.setCardId(cardId);
}
private String getCardId(String name) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "435467523543" + Thread.currentThread().getId();
}
}
public final class ActionContext { //写成final好些,工具类来说的话
private static final ThreadLocal<Context> threadLocal =
new ThreadLocal<Context>() {
@Override
protected Context initialValue() {
return new Context();
}
};
private static class ContextHolder {
private final static ActionContext actionContext = new ActionContext();
}
public static ActionContext getActionContext() {
return ContextHolder.actionContext;
}
public Context getContext() {
return threadLocal.get();
}
private ActionContext(){
}
}