共有可変状態
複数のスレッドから読み書きされる、書き換え可能なデータ。競合はこれと複数スレッドが揃ったときにだけ起きる。
Javaメモリモデルとhappens-before
共有データの見え方を定める規則。あるステートメントによるメモリ書き込みが、別のステートメントから見えることを保証する関係をhappens-beforeという。
競合状態(race condition)
複数のスレッドがどの順序で特定のコードに到達するかによって結果が変わってしまう不具合。
レースコンディション
メモリ一貫性エラー
同じデータに対して複数のスレッドが一貫しない見え方をすることで生じる誤り。古い値が見え続けたり、書き込みの順序が入れ替わって見えたりする。
happens-before関係
あるスレッドのメモリ書き込みが別スレッドの読み取りから見えることを保証する、Java言語仕様が定める順序関係。synchronized・volatile・java.util.concurrentの各機能が確立する。
volatile
最適化による読み書きの除去を抑える型修飾子。スレッド間の同期を保証するものではない。
Atomic系クラス(AtomicInteger / LongAdder)
単一変数の原子的更新を提供するJavaのクラス群。LongAdderは内部でセルを分割して競合を散らす。
ExecutorService
仕事(Runnable / Callable)と、それをどう実行するかを分離し、スレッドの生成・再利用・破棄をライブラリに任せるJavaの実行基盤。
スレッドの自作(new Thread)
上位のAPIに任せず、スレッドオブジェクトを直接生成してライフサイクルを自分で管理する書き方。
backpressure
流し込みが速すぎるときに、書き込み側を待たせて増えすぎを防ぐ仕組み。
バックプレッシャー
仮想スレッド
Javaランタイムが実装する軽量スレッド。JDKのブロッキング操作の間はOSスレッドを手放すため、数百万本を1つのJVMで走らせられる。JDK 21で正式機能になった。
I/Oバウンドなタスク
処理時間の大半がネットワークやディスクなどI/Oの完了待ちで占められるタスク。
CPUバウンドなタスク
処理時間の大半が計算で占められ、I/O待ちがほとんどないタスク。並列化の効果はコア数で頭打ちになる。
Semaphoreによる同時実行数制限
「外部APIへ同時10接続まで」のような制限を、プールのサイズではなくセマフォの許可数で表現する形。
同時実行数の制限
外部APIやデータベースなどの資源に対して、同時に走る処理の数へ上限を設けること。
キャリアスレッドへのピン留め
仮想スレッドがブロックしてもキャリアとなるOSスレッドを手放せず、占有し続けてしまう状態。
synchronizedブロック
Javaの組み込みモニターによる相互排他構文。ロック対象のオブジェクトを指定して排他区間を作る。
専用ロックオブジェクト
外部に公開しない、ロック専用のインスタンス。.NET 9 / C# 13以降はSystem.Threading.Lock型を使う。
ReentrantLock
タイムアウト付き取得(tryLock)、公平性ポリシー、複数のConditionを扱えるJavaの明示的ロック。
ロック取得順序の統一
複数のロックを取るとき、全スレッドが同じ順序で取ると決めるルール。
デッドロック(deadlock)
複数のタスクが互いの完了やリソース解放を待ち続け、永遠に進行しなくなる状態である。
スレッドローカル状態
各スレッドが自分だけの領域に持つ中間状態。並列ループで共有メモリへ書き込まずに集計するために使う。
ConcurrentHashMap.computeIfAbsent
キーが不在ならマッピング関数を呼んで値を登録する、呼び出し全体が原子的に実行されるメソッド。
BlockingQueue
容量を指定できるJavaのブロッキングキュー。満杯時のputがブロックすることで背圧になる。
割り込み(interruption)
対象スレッドの割り込みステータスを立て、sleep・wait・joinでブロック中ならInterruptedExceptionで起こす、Javaの停止・キャンセルの共通言語。
協調キャンセル
キャンセルを強制ではなく要求として伝え、停止の仕方はリスナー側が決めるモデル。
Thread.stop / suspend / resume
スレッドを外から強制的に止めようとした旧世代のAPI。不正な状態のままロックが解放されるなど本質的に安全でない。
ExecutorServiceの2段階シャットダウン
shutdown()で新規受付を止めて完走を待ち、期限を過ぎたらshutdownNow()で停止を試みる停止手順。
期限内の停止処理
サービス停止やアプリ終了までの限られた時間内に、スレッドプールの停止を確実に完了させること。
イベントディスパッチスレッド(EDT)
Swingのイベント処理コードが走る専有スレッド。ほとんどのSwingオブジェクトのメソッドはスレッドセーフではない。
UIスレッドのコンテキスト
WinForms/WPFなどデスクトップアプリで、UIの応答性を保つために画面更新や重い処理の実行場所として意識されるコンテキスト。
UIイベントハンドラ
スレッドダンプ(jstack / jcmd)
指定したJavaプロセスの全スレッドのスタックトレースを出力する標準ツール。-lでロックに関する追加情報も出せる。
構造化並行性(StructuredTaskScope)
関連するサブタスク群を1つの作業単位として扱い、エラー伝播とキャンセルを構造化するJavaのAPI。
Scoped Values
スレッド内およびスレッド間で不変のコンテキストデータを安全かつ効率的に共有する仕組み。ThreadLocalの可変性・寿命管理・継承コストへの解決策としてJDK 25で正式化された。
count++は読み込み・加算・書き戻しの3ステップに分かれるため、2つのスレッドが同時に入ると一方の加算が上書きされて消える。
同期なしに共有変数を読み書きすると、同じデータに対して複数のスレッドが一貫しない見え方をする。ループの中でbooleanフラグを見ているのに他スレッドが変えた値がいつまでも見えない、という現象はメモリモデルが許す動作でJVMのバグではない。
Javaメモリモデルはhappens-before関係によって、あるステートメントによるメモリ書き込みが別のステートメントから見えることを定義する。
メモリ一貫性エラーを避ける鍵はhappens-before関係を正しく作ることで、synchronized・volatile・java.util.concurrentの各クラスがこの関係を確立する。同期なしではメモリモデルは古い値や並べ替えられた観測を許す。
volatileは可視性と順序(happens-before)の保証であって、複合操作の原子性は与えない。フラグと単一値はAtomic系、複合状態はロックにする。
単一変数の原子的更新はAtomicInteger / AtomicLong / AtomicReferenceが受け持ち、高頻度に加算するだけの統計値なら競合に強いLongAdderを使う。
仕事(Runnable / Callable)と、それを何本のスレッドでどのキューで実行するかを分離し、スレッドの生成・再利用・破棄をライブラリに任せる。
固定プールはスレッド数を制限するが待ち行列は無制限なので、投入が処理を上回り続ける常駐サービスではキューに積まれたタスクがメモリを食い続ける。容量付きキューと拒否ポリシー、または投入側の入場制限で背圧をかける。
JDKのブロッキング操作の間はOSスレッドを手放すため、I/O待ちが主体のタスクは「タスク1つに仮想スレッド1本」で走らせる。newVirtualThreadPerTaskExecutorの形で使い、決してプールしない。
仮想スレッドは速いスレッドではなくスケール(スループット)のための機能で、コードの実行速度は変わらない。CPUバウンドの並列化は従来どおりコア数程度のプラットフォームスレッドが適任である。
「外部APIへ同時10接続まで」のような同時実行数の制限は、プールのサイズではなくSemaphoreで表現する。
JDK 21〜23ではsynchronizedブロック・メソッド内でブロックすると仮想スレッドがキャリアスレッドにピン留めされ、スケールの利点が失われる制約があった。
JDK 24でJVMのモニター実装が仮想スレッド対応に書き直され、synchronizedブロック・メソッド内でブロックしても仮想スレッドがピン留めされなくなった。ピン留め対策としての機械的なReentrantLockへの置き換えは不要になっている。
synchronized(this)やsynchronized(SomeClass.class)は外部のコードが同じオブジェクトをロックできてしまうため、外部に公開しないprivate finalのロックオブジェクトを守りたいデータと1対1で対応させる。
タイムアウト付き取得(tryLock)、公平性ポリシー、複数のCondition、ロックの取得と解放を別メソッドに分けたい場合はReentrantLockに進む。lock()の直後にtry、finallyでunlock()という形を崩さない。
2つ以上のロックを取る場所では全スレッドが同じ順序で取ることをルール化し、順序を保証できない箇所にはtryLock(timeout)で取れなければ手放してやり直す経路を用意する。
並列集計では共有の合計変数に各スレッドが書き込むのではなく、スレッドごとに部分結果を作って最後に合流させる。LongAdderも内部でセルを分割して競合を散らし、読むときに合算する実装である。
computeIfAbsentはメソッド呼び出し全体が原子的に実行され、キー不在時にマッピング関数がその1回の呼び出しの中でちょうど1回だけ呼ばれる。原子的である代償として計算中は他スレッドの一部の更新がブロックされるため、マッピング関数は短く保ち、関数の中でこのマップ自体を更新してはいけない。
容量を指定したArrayBlockingQueueなら、満杯時にputがブロックして自然な背圧になる。プロデューサー/コンシューマーの境界を明確にするこの設計は仮想スレッド時代でも有効である。
interrupt()は対象スレッドの割り込みステータスを立て、sleep・wait・joinなどでブロックしていればInterruptedExceptionを投げて即座に起こす(このときステータスはクリアされる)。InterruptedExceptionをcatchして何もしないコードを書かず、終了できないならステータスを復元して呼び出し元へ合図を伝える。
Thread.stop / suspend / resumeは、不正な状態のままロックが解放されて壊れたオブジェクトが見えたり、suspendがデッドロックを招いたりするため本質的に安全でなく、現在は呼ぶとUnsupportedOperationExceptionになる。
shutdown()は投入済みタスクを完走させつつ新規受付を止め、shutdownNow()は実行中タスクの停止を試みる。これはベストエフォートで、典型的な実装はThread.interrupt()でキャンセルするため、割り込みに応答しないタスクはshutdownNowでも止まらない。
公式ドキュメントが示す停止の定石は、shutdown → awaitTermination → shutdownNow の2段階パターンである。JDK 19以降のclose()はタイムアウトなしで完了を待つため、必ず有限時間で終えたい停止経路には期限付きの2段階パターンを使う。
Swingのイベント処理コードはEDT上で走り、ほとんどのSwingオブジェクトのメソッドはスレッドセーフではないため、複数スレッドから触るとスレッド干渉やメモリ一貫性エラーを招く。別スレッドからの画面更新はSwingUtilities.invokeLaterでEDTに依頼する。
jstack(またはjcmd Thread.print)が全スレッドのスタックトレースを出力し、-lでロックに関する追加情報まで表示できる。ダンプを数秒おきに2〜3回取り、動いていないスレッドがどのロックを待ち、そのロックを誰が握っているかを突き合わせる。
従来形式のスレッドダンプはプラットフォームスレッド向けで、アプリの仮想スレッドは含まれない。仮想スレッドを使う構成では jcmd Thread.dump_to_file -format=json を使う。
構造化並行性APIは関連するサブタスク群を1つの作業単位として扱い、エラー伝播とキャンセルを構造化する。JDK 25の第5次プレビューでStructuredTaskScope.open()の形に改められ、JDK 26でも第6次プレビューとして継続しているため、利用にはプレビュー機能の有効化が必要になる。
Scoped ValuesはJDK 25で正式化され、スレッド内およびスレッド間で不変のコンテキストデータを安全かつ効率的に共有する。ThreadLocalの可変性・寿命管理・継承コストへの解決策として位置づけられている。