まず ThreadLocal について理解する
まず問題があります。マルチスレッド環境では、複数のスレッドが同時に共有変数にアクセスする必要がありますが、共有変数に対する各スレッドの関連操作が、そのスレッドにのみ表示されることを望んでいます。現在のスレッドです。どうすればよいでしょうか?
実際、ThreadLocal は、この問題を解決するために使用されるツールです。スレッドごとに独立したストレージ スペースを提供します。このスペースは、共有変数のコピーを保存するために使用されます。その後、各スレッドは、共有変数のコピーに対してのみ動作します。共有変数であり、その操作は他のスレッドからは見えません。
したがって、ThreadLocal をスレッド ローカル変数と呼ぶことができます。これは、ThreadLocal 変数を作成した後、この変数にアクセスする各スレッドがこの変数のローカル コピーを持つことになると言うのと同じです。複数のスレッドがこの変数で動作する場合、それらは実際には独自のローカル メモリ変数で動作するため、スレッド分離の役割を果たし、スレッドの安全性の問題を回避します。
特定のコード例を通して、次のことを見てみましょう。
public class ThreadLocalExample {
public final static ThreadLocal<String> STRING_THREAD_LOCAL = ThreadLocal.withInitial(()->"DEFAULT VALUE");
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+":INITIAL_VALUE->"+STRING_THREAD_LOCAL.get());
STRING_THREAD_LOCAL.set("Main Thread Value");
System.out.println(Thread.currentThread().getName()+":BEFORE->"+STRING_THREAD_LOCAL.get());
Thread t1 = new Thread(()->{
String value = STRING_THREAD_LOCAL.get();
if (value == null){
STRING_THREAD_LOCAL.set("T1 Thread Value");
}
System.out.println(Thread.currentThread().getName()+":T1->"+STRING_THREAD_LOCAL.get());
},"t1");
Thread t2 = new Thread(()->{
String value = STRING_THREAD_LOCAL.get();
if (value == null){
STRING_THREAD_LOCAL.set("T2 Thread Value");
}
System.out.println(Thread.currentThread().getName()+":T2->"+STRING_THREAD_LOCAL.get());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Thread.currentThread().getName()+":AFTER->"+STRING_THREAD_LOCAL.get());
}
}
このケースの主なロジックは次のとおりです。
- まず、グローバル String 型 ThreadLocal オブジェクト STRING_THREAD_LOCAL を定義します。初期値は DEFAULT_VALUE です。
- メインスレッドの最初の行では、STRING_THREAD_LOCAL.get() メソッドを通じてデフォルト値が渡されます。
- メインスレッドでは、STRING_THREAD_LOCAL.set("Main Thread Value") を通じて値が設定され、ThreadLocal に保存されます。
- 次に、2 つのスレッド t1 と t2 を定義し、それぞれ STRING_THREAD_LOCAL.set を使用して 2 つのスレッドに新しい値を設定し、各スレッドで設定した後の結果を出力します。
- 2 つのスレッド t1 と t2 の実行が終了した後、メイン スレッドの STRING_THREAD_LOCAL.get() を通じて現在の ThreadLocal の値を出力し続けます。
この事件の結果は次のとおりです。
main:INITIAL_VALUE->DEFAULT VALUE
main:BEFORE->Main Thread Value
t1:T1->DEFAULT VALUE
t2:T2->DEFAULT VALUE
main:AFTER->Main Thread Value
結果から、異なるスレッドの STRING_THREAD_LOCAL.set() メソッドによって設定された値は現在のスレッドにのみ表示され、スレッドは相互に影響を与えないことがわかり、驚きました。これは ThreadLocal の役割であり、異なるスレッド間でデータの分離を実現できるため、共有変数に対するマルチスレッド操作のセキュリティが確保されます。
ThreadLocalのメモリ構造図
ThreadLocalのアプリケーションシナリオ分析
Spring-JDBC の TransactionSynchronizationManager クラスでは、ThreadLocal を使用してデータベース接続とトランザクション リソースを確実に分離し、異なるスレッド間のトランザクションと接続の混乱を回避します。
実際の開発では、ユーザーがログインした後、インターセプターがユーザーの基本情報を取得し、後続のメソッドで使用しますが、HttpServletRequest が設定されている場合、柔軟性はあまり高くなく、サーバー オブジェクトにも依存します。今回はThreadLocalでも使用できます。インターセプタでは、検証後、ユーザーの情報を ThreadLocal に保存し、後続のコードは get を通じてユーザーの情報を直接取得できます。
ThreadLocal は SimpleDateFormat のスレッド安全性の問題を解決します
コードでそれをシミュレートしてみましょう。
public class SimpleDateFormatExample {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
public static void main(String[] args){
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 9; i++) {
executorService.execute(()->{
try {
System.out.println(parse("2021-06-15 16:35:20"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
}
SimpleDataFormatExample クラスを使用してスレッド プールを構築し、スレッド プールに文字列解析タスクを 9 サイクルにわたって実行させます。上記のプログラムを実行すると、次の例外情報が表示される場合があります。
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at Example.SimpleDateFormatExample.parse(SimpleDateFormatExample.java:13)
at Example.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)
例外の理由は、SimpleDateFormat がスレッドセーフではないためであり、SimpleDateFormat の公式ドキュメントには次のように説明されています。
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.
DateFormats不是一个满足同步机制的类,因此建议为每个线程创建单独的实例,如果有多个线程同时访问,则必须要通过外部的同步机制来进行保护,保证线程安全。
SimpleDateFormat のクラス図を見てみましょう。
SimpleDateFormat は DateFormat クラスを継承し、DateFormat で 2 つのグローバル メンバー変数を定義します。
Calendar と NumberFormat は、それぞれ日付と数値を変換するために使用されます。
public abstract class DateFormat extends Format {
protected Calendar calendar;
protected NumberFormat numberFormat;
}
DateFormat のクラス アノテーションにもスレッド非安全なステートメントが見つかりました。
Date formats are not synchronized. It is recommended to create separate format instances
for each thread. If multiple threads access a format concurrently, it must be synchronized
externally.
Calendar と NumberFormat の 2 つのメンバー変数はスレッド セーフではありません。つまり、マルチスレッド環境で 2 つのメンバー変数を操作すると、スレッド セーフの問題が発生します。特定のエラー メッセージを通じて問題の根本を特定できます。 。
SimpleDateFormat クラスの SimpleDateFormat.subParse(SimpleDateFormat.java:1869)。numberFormat は解析操作に使用されます。
private int subParse(String text, int start, int patternCharIndex, int count,
boolean obeyCount, boolean[] ambiguousYear,
ParsePosition origPos,
boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) {
//省略
//内部调用numberFormat的parse()方法,转化数字
//这里的numberFormat就是DateFormat中的成员变量,默认实例是DecimalFormat
number = numberFormat.parse(text, pos);
if(number != null){
value = number.intValue();
}
return -1;
}
引き続き、numberFormat.parse() メソッドの実装を確認してください。
public Number parse(String text, ParsePosition pos) {
//内部调用subparse()方法,将text的内容“set”到digitList上
if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
return null;
}
//将digitList转为目标格式
if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
gotDouble = false;
longResult = digitList.getLong();
if (longResult < 0) { // got Long.MIN_VALUE
gotLongMinimum = true;
}
} else {
doubleResult = digitList.getDouble();
}
return gotDouble ?
(Number)new Double(doubleResult) : (Number)new Long(longResult);
}
}
ここで、digitList はグローバル変数です。
private transient DigitList digitList = new DigitList();
引き続き、DecimalFormat の subparse() メソッドを見てみましょう。
private final boolean subparse(String text, ParsePosition parsePosition,
String positivePrefix, String negativePrefix,
DigitList digits, boolean isExponent,
//digitList在这个方法里面叫digits,先对digits进行清零处理
digits.decimalAt = digits.count = 0;
backup = -1;
//还要对digits继续操作
if (!sawDecimal) {
digits.decimalAt = digitCount; // Not digits.count!
}
digits.decimalAt += exponent;
return true;
}
ここから、エラーの根本原因は、subParse() メソッドでグローバル変数の数字の更新操作がロックされておらず、原子性を満たしていないことであることがわかります。ThreadA と ThreadB が同時に subParse() メソッドに入り、同時にグローバル変数 digitList を更新すると仮定すると、ThreadA は CASE2 の位置まで実行され、ThreadB は CASE 1 の位置まで実行されるだけである可能性があります。 DecimalFormat.parse()メソッドで取得したdigitList 例外が発生する問題が発生しました。
参考書:
「Java同時プログラミングの徹底した分析と実践」
「Javaプログラミングの考え方」
「Java同時プログラミング実践」