よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

static と synchronized から始めるマルチスレッド入門 〜中編:マルチスレッドの基本的な用語と概念〜

初めに

本記事は3部構成になっております。

  • 『前編:static とメモリ管理』

static と synchronized から始めるマルチスレッド入門 〜前編〜 - よしたろうブログ

  • 『中編:マルチスレッドの基本的な用語と概念』

static と synchronized から始めるマルチスレッド入門 〜前編〜 - よしたろうブログ

  • 『後編:synchronized の使い方と注意点』

static と synchronized から始めるマルチスレッド入門 〜後編〜 - よしたろうブログ

本記事では実務の中で使用している静的解析ツール(SonarLint)に指摘された警告の修正を入り口に、マルチスレッドについて説明したいと思います。

本記事では、以下の事柄について解説していきます。

  • static の意味
    • メモリ管理の方法
    • メモリ上にプログラムを展開する意味と理由
    • atatic とインスタンスの違い
    • static の使い所と注意点
  • マルチスレッドの基本的な話
    • プロセスとは
    • スレッドとは
    • スレッドセーフとは
    • 並行と並列
  • 同期制御と排他制御の概論と基本
  • アトミック性と可視性
  • synchronizedとは
    • インクリメントは非アトミックな操作
    • synchronized はミューテックス(単一ロック)
    • ミューテックスはパフォーマンスには良くない
    • synchronized メソッドと synchronized ブロックの違い
    • その他の注意点

取り扱わない話

  • 同期制御と排他制御の各論
  • Thread クラス
  • ReentrantLock クラス
  • Atomic クラス
  • wait()/notify()
  • Executorフレームワーク
  • 不変オブジェクト
    • マルチスレッドにおける不変オブジェクトについては以下の過去記事にて解説してます。

目次

前編は1~3、中編は4~5、後編は6~9 です。

4. マルチスレッドの基本的な話

マルチスレッドには多くの用語が登場するので語彙の定義をまず示したいと思います。

マルチスレッドプログラミングに関連する用語の定義と詳細を以下に解説します。

4-1. 図表

詳細な解説の前にまず簡単に定義を紹介します。

用語 

定義 

プロセス(Process)

命令(プログラム)の実行単位。実行中プログラムのインスタンスであり、メモリ空間やCPUなどのシステムリソースを割り当てられた単位。

スレッド(Thread)

プロセスの軽量な実行単位として登場した「軽量なプロセス」である。スレッドはプロセスに含まれる。あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成されるため、スレッドの方が起動のオーバーヘッドが小さい。プログラム内で処理を行う独立した命令実行の流れを持つ単位で、CPUが処理する最小単位。プロセスと共有されるメモリ空間があるが、独自のスタックとCPUレジスタといった独自のメモリ空間を持つ。

メインスレッド(Main Thread)

プログラムの最初に実行されるスレッドであり、プログラムの制御を担当。

スレッドセーフ(Thread Safe)

複数のスレッドからメソッド・フィールドや共有データ(インスタンスなど)にアクセスされても問題が起きない状態、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。

並列性(parallelism)

2 つ以上のスレッドが同時に実行されている状態を表す概念。

並行性(concurrency)

2 つ以上のスレッドが進行過程の実行状態にあることを表す概念。一般化された形の並列性で、疑似的に複数の処理が同時に実行されているように見える。

シングルスレッド化

1 プロセス 1 スレッドで動作させること。ある処理を単一のスレッドのみを用いて動作させる環境もしくは手法

マルチスレッド化

1 プロセス複数スレッドで動作させること。複数のスレッドが同時に動作することで、並行処理や並列処理を実現できる。

クリティカルセクション (critical section)

複数の処理が同時期に実行されると競合状態を起こす単一の共有データ、コードセクションを指す。つまり、複数のスレッドが同時にアクセスしてはならないデータまたはリソースのこと。

同期制御 (Synchronization)

クリティカルセクションへのアクセスを制御するプロセス。複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。排他制御を包含する。

排他制御 (Exclusive Control)

排他制御とは、クリティカルセクションへの複数プロセス(またはスレッド)が同時に入ることを防ぐ。複数のプログラムスレッドやプロセスが同時に共有リソースにアクセスすることを防ぐために、リソースの使用権限を制御する。

4-2. プロセス(Process)

定義
命令(プログラム)の実行単位。プログラム実行時、OSからメモリを割り当てられ、プログラムコード、データ、スタックなどの情報を保持する仮想アドレス空間を持つ。実行中プログラムのインスタンスであり、メモリ空間やCPUなどのシステムリソースを割り当てられた単位。

解説
プログラムが実行されるためにはプログラムのコードやデータ・スタックなどがメモリ上に展開される必要がある(上記で記述済み)。メモリ上にプログラムが展開される(インスタンスや static)ことで、CPUがそれらを実行可能となる、プログラムが実行されている状態を指します。プロセスは、このような実行中のプログラムを表す仮想的な概念。OSが提供する様々な機能やリソースを利用して、プログラム実行環境を構築する。

プロセスは、(複数の)スレッドを含めた複数の命令実行の流れを持つ。新たなプロセスを動作させるためには、CPUやメインメモリ上のアドレス空間などの計算資源(リソース)を割り当てる。これは都度、親プロセスをfork(copyと同義)して、子プロセスとして独自の仮想メモリを割り当てるため親プロセスを頂点としたツリー構造を構築する。そのため各プロセスは独立しており、他のプロセスのメモリに直接アクセスできない。プロセス間でデータを共有するには、共有メモリやパイプ、ソケット通信などのメカニズムを使用する必要がある。 スクリーンショット 2023-05-07 19.27.05.png

ただし、マルチプロセスであろうが共有データ(DBやファイルなど)への対応はマルチスレッドと同じくデータ不整合・競合状態(競合条件)が発生しないよう、適切な同期処理が必要。上記での話はあくまでプロセスのメモリ空間の話。

4-3. スレッド(Thread)

定義
プロセスの軽量な実行単位として登場した「軽量なプロセス」である。スレッドはプロセスに含まれる。あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成されるため、スレッドの方が起動のオーバーヘッドが小さい。プログラム内で処理を行う独立した命令実行の流れを持つ単位で、CPUが処理する最小単位。プロセスと共有されるメモリ空間があるが、独自のスタックとCPUレジスタといった独自のメモリ空間を持つ。

解説
スレッドはプロセス内で並行して実行できる命令の流れ。プロセスはスレッドの集合であり、同じプロセスに属するスレッド間の通信は簡単に実行できる。なぜなら、それらのスレッドはメモリ空間を含めあらゆるものを共有しているから(共有メモリ形式)。したがって、あるスレッドで生成されたデータを、他のすべてのスレッドがただちに利用できる。同一プロセス内の複数スレッドを同一メモリ空間上で実行でき、メモリ消費量などが軽減できるしスレッドの切り替えに要する時間も、プロセスの切り替えに要する時間よりも短くて済む。スレッド切替にはメモリ空間を切り替える必要がない。同じデータにアクセスしながら並行動作するような複数の処理には、マルチスレッドを使った方がプログラミングは断然楽になる。共有メモリとして利用できるのは ヒープ領域に置かれたインスタンスやクラスフィールドなどになる。

ただし、これがスレッドセーフかどうかを引き起こす原因である。単一プロセスに対するマルチスレッド処理プログラミングにおいて、同じデータを複数のスレッドが同時に書き換えることによる不整合に注意し、同期・排他制御、スレッド間データ共有を防ぐか不変オブジェクトの使用などを行う必要が発生する。

  • スレッドを使用するメリット
    • パフォーマンス向上
      • 複数のタスクを同時に実行できるため、パフォーマンスを向上させることができる。
    • 応答性の向上
      • ユーザーの入力にすぐに応答できるため、応答性を向上できる。
    • スケーラビリティの向上
      • スレッドは、複数のコンピューター上で実行できるため、スケーラビリティを向上させることができる。
  • スレッドを使用するデメリット
    • 複雑さの増加
      • スレッドは複雑になる可能性があるため、管理が難しい場合がある。
    • 競合の可能性
      • スレッドは競合する可能性があるため、予期しない結果が発生する可能性がある。
    • メモリ使用量の増加
      • スレッドはメモリを大量に消費する可能性があるため、メモリ使用量が増加する可能性がある。

4-4. プロセスとスレッドの違い

解説
OSによってプロセスは管理され、プロセスによってスレッドは管理される。スレッドはプロセスの内部での話。プロセスがスレッドを含んでいるという関係で、単一プロセスは複数スレッドを持つことができる。そして複数のスレッドが1プロセス内で実行されることをマルチスレッドと呼ばれる。

また、マルチプロセスはプロセスごとにメモリ空間が独立しているため、あるプロセスから別のプロセスが参照しているメモリに直接アクセスはできない。対してマルチスレッドの場合、あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成される。単一空間内のメモリを共有しながら複数の処理を行なう「共有メモリ方式」である。

プロセスとスレッドの主な違いは、プロセスは独立した実行単位であり、スレッドはプロセス内の依存関係のある実行単位であるということ。

プロセスとスレッドの利点・欠点は以下

  • プロセス
    • 利点
      • セキュリティ:プロセスは相互に隔離されているため、セキュリティが向上する。
        • プロセスは相互に隔離されているため、あるプロセスが別のプロセスに害を及ぼすことができない。
      • スケーラビリティ:プロセスは複数のサーバーにスケーリングできる。
      • 回復力:プロセスがクラッシュしても、他のプロセスには影響しない。
    • 欠点
      • 重量級:プロセスはスレッドよりも重いため、起動と実行に時間がかかる。
      • 複雑さ:プロセスはスレッドよりも複雑であるため、管理が難しい場合がある。
  • スレッド
    • 利点
      • 軽量:スレッドはプロセスよりも軽量であるため、起動と実行が高速。
      • 効率:スレッドは並行処理に使用できるため、効率を向上させることができる。
    • 欠点:
      • 競合の可能性:スレッドは競合する可能性があるため、予期しない結果が発生する可能性がある。
        • 競合は、データの損失、予期しない結果、およびアプリケーションのクラッシュを引き起こす可能性がある。
        • 複数のスレッドが同じアプリケーションを実行している場合、それらをすべて追跡することが困難になる可能性がある。

4-5. メインスレッド(Main Thread)

定義
プログラムの最初に実行されるスレッドであり、プログラムの制御を担当。

解説
メインスレッドが終了すると、プログラム全体が終了。

4-6. スレッドセーフ(Thread Safe)

定義
複数のスレッドからメソッド・フィールドや共有データ(インスタンスなど)にアクセスされても問題が起きない状態、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。

解説
スレッドセーフでないリソースを複数スレッドからアクセスする場合には同期・排他制御を行わなくてはならない。

  1. アクセスされるリソースのクラスをスレッドセーフにする
  2. アクセスするクラスで排他制御を行う

一般には1つ目の方法のほうが、排他制御を行う場所を局所化できるという理由から望ましいと言われる。デッドロックの可能性、パフォーマンスの低下の際の原因検索や切り分けが容易になる。また、排他制御を行うコードを書く場所を凝集できるため、変更容易性も高い。

名著「Effective Java」では以下の様にスレッドセーフレベルが定義されている。

  • 不変 (immutable) - このクラスのインスタンスは、定数のように見えます。 外部での同期は必 要ありません。例としては、 String Integer, BigInteger があります (項目15)。
  • 無条件スレッドセーフ (unconditionally thread-safe) - このクラスのインスタンスは可変です が、すべてのメソッドは、インスタンスが外部同期を必要とすることなく並行して使用できるよう に、十分な内部同期を含んでいます。 例としては Random や ConcurrentHashMap があります。
  • 条件付きスレッドセーフ (conditionally thread-safe) - 無条件スレッドセーフと似ていますが、 安全に並行して使用するために、メソッドのいくつかは外部同期を必要とします。例としては、外 部同期を必要とするイテレータを持つ Collections.synchronized ラッパーが返すコレクションがあり、それらのコレクションのイテレータは外部同期を必要とします。
  • スレッドセーフでない (not thread-safe) - このクラスのインスタンスは、可変です。並行して 使用するためには、クライアントは個々のメソッド呼び出し (あるいは、一連のメソッド呼び出し) をクライアントが選択している外部同期で囲まなければなりません。例としては、ArrayList や HashMap などの汎用コレクション実装があります。
  • スレッド敵対 (thread-hostile) - このクラスは、たとえすべてのメソッドが外部同期で囲まれたとしても、並行した使用では安全ではありません。 一般に、スレッド敵対は、 static のデータを同期なしで変更することに起因しています。

マルチスレッドにおける不変オブジェクトについては以下の過去記事にて解説してます。

4-7. 並列性(parallelism)

定義
2 つ以上のスレッドが同時に実行されている状態を表す概念。

解説
並行性を包含する。違いは実行状態であり、並列はまさしく同時に実行されるタスクの数が複数ある状態を指す。例えば、マルチコアのCPUで複数のスレッドを同時に実行することや、複数のプロセスを同時に実行することが並列処理にあたる。対して、並行はタイムスライス毎にタスクをスイッチし、各タスクは常に実行状態にある。

4-8. 並行性(concurrency)

定義
2 つ以上のスレッドが進行過程の実行状態にあることを表す概念。一般化された形の並列性で、疑似的に複数の処理が同時に実行されているように見える。

解説
複数のプロセスやスレッドが同時に進行しているように見えるが、実際にはシステムリソースが単一のプロセッサ(CPU)やコアで共有され、タイムスライスで分割されて交互に実行されることを指す。タイムスライスとは、プロセッサの時間配分単位であり、あるプロセスが一定時間内に使用できるプロセッサ時間の量を表す。タイムスライスは人間にとってはかなり短く、プロセスやスレッドの切り替えは人間にとっては知覚できない時間感覚なので、プロセスは同時実行されているように感じる。

  • 引用元(ちょい編集):並行処理(Concurrency) vs. 並列処理(Parallelism)

    並行処理(Concurrency)とは、必ずしも同時に実行する必要はないが、多くのタスクを担当することができるシステム。順番に玉ねぎをきざんでフライパンに放り込み、それが炒め上がるまでの間にトマトを切ったりできます。 並列処理(Parallelism)とは、片手で玉ねぎの入ったフライパンを振りながらもう片方の手でトマトを切るようなもの

4-9. シングルスレッド化

定義
1 プロセス 1 スレッドで動作させること。ある処理を単一のスレッドのみを用いて動作させる環境もしくは手法。

4-10. マルチスレッド化

定義
1 プロセス複数スレッドで動作させること。複数のスレッドが同時に動作することで、並行処理や並列処理を実現できる。

4-11. 並列性と並行性の違い

以下の引用が分かり易すぎたのでそのまま引用しています。

引用元:第1回 マルチスレッドはこんなときに使う

image.png

 このように、複数のCPUで処理を分担できれば、すべての処理が終了するまでの処理時間は向上するわけであるが、1つのCPUで並行処理をさせると、疑似的に処理を分担しているように見えるだけであるので、実際にはすべての処理が終了するまでの時間は変わらず、むしろ処理切り替えのために悪化する可能性もある。

 上図では、処理Aと処理Bが両方とも終了するのは(3)のマルチCPUによるマルチスレッドが一番早く、(1)のシングルCPUによるシングルスレッドの場合と(2)のシングルCPUによるマルチスレッドの場合は(ほぼ)同じとなる。一方、処理Bの終了する時間に着目すると、(1)と(3)の場合がともに早く、(2)が一番遅い。つまり、(2)のシングルCPUによるマルチスレッドがパフォーマンスとしては、一番遅くなってしまうともいえるのである。

4-12. クリティカルセクション (critical section)

定義
複数の処理が同時期に実行されると競合状態を起こす単一の共有データ、コードセクションを指す。つまり、複数のスレッドが同時にアクセスしてはならないデータまたはリソースのこと。

解説
データベースや共有メモリ、ファイル、ネットワーク接続など、様々な形で表現されるリソース全般が対象になる。データの同一性が保証されなくなる可能性がある場合は、クリティカルセクションでは常に排他制御を行なう必要がある。プロセス内の共有データ(クリティカルセクション)に複数のスレッドがアクセスする可能性がある場合は、スレッド間の排他制御を行ない、アトミック性を確保する必要がある。

4-13. 同期制御(Synchronization)

定義
クリティカルセクションへのアクセスを制御する。複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。排他制御を包含する。

解説
排他制御を行ううえで最も気を付けなくてはならないデッドロックデッドロックはアプリケーション内部で排他制御などによる競合が起こり、アプリケーションが止まってしまう(反応がなくなってしまう)状態。そのためスレッド同士がタイミングを計って協調動作しなければならないときがある。このような制御を「同期制御」と呼ぶ。Java における同期制御の実現には以下の方法がある。

  1. synchronized ブロック(後述)
  2. synchronized メソッド(後述)
  3. volatile 変数(使用可能箇所は限定的)
    1. その変数への書き込みが、その変数の現在の値に依存しない
    2. その変数が、他の変数との不等式に使われない
      1. インクリメント演算には使えない
      2. Compare and Assignment には使えない
    3. 参考:『Java の理論と実践: volatile を扱う』を読んで
    4. 参考: [Java] volatile 変数
  4. Atomic クラス(本記事では解説なし)

4-14. 排他制御 (Exclusive Control)

定義   排他制御とは、クリティカルセクションへの複数プロセス(またはスレッド)が同時に入ることを防ぐ。複数のプログラムスレッドやプロセスが同時に共有リソースにアクセスすることを防ぐために、リソースの使用権限を制御する。

解説   共有データに同時アクセスすると、データ破損や予期しない動作が発生する可能性がある。そのため、プログラムが共有データにアクセスする際には、他のプログラムとの競合を避けるために排他制御を行う必要がある。排他制御には後述するロックを行う必要がある。ロックにはミューテックスセマフォ、スピンロックなどの手法がある。Java における排他制御及びロックの実現には以下の方法などがある。

  1. synchronized キーワード
  2. Lock インターフェイス

排他制御はパフォーマンスの劣化を招く

排他制御を行うと、プログラムの実行パフォーマンスが悪くなる。

  1. 排他制御の仕組みそのものが原因
    1. マルチスレッドによる並行処理によってパフォーマンス改善の目指しているが、排他制御ではその並行処理を部分的に並行で動作しないように制御するということを行っている。つまりデータの整合性を保つために、部分的にマルチスレッドによるパフォーマンスの利点をつぶすことになる。
  2.  lock インターフェイスによるパフォーマンス低下
    1. lock インターフェイストを実行してロックを取得する場合の実行コストは小さくない

基本的には、排他制御によるパフォーマンスの低下をできるだけ少なくするために、まずはロックによる排他制御を行わなくてもよいような設計を検討した方がよい。それでも排他制御を行わなくてはいけない個所においては、できるだけロックする範囲と時間を小さくするとよい。ただし、ロックフリーなアルゴリズムなど様々な方法があり、どの方法でも銀の弾丸はやはりない。

参考:フリー百科事典『ウィキペディア(Wikipedia)』 Lock-freeとWait-freeアルゴリズム

5. アトミック性と可視性

アトミック性と可視性は、Javaのマルチスレッドプログラミングにおいて非常に重要な概念です。同期・排他制御をコードで実現するためには必須の知識となります。

5-1. 図表

詳細な解説の前にまず簡単に定義を紹介します。

用語 

定義 

競合状態(race condition)

複数スレッドでの処理が、クリティカルセクションに同時アクセスした場合に、データの不整合が起き、結果的にシステム停止など予期しない処理結果が生じてしまうこと。競合状態になるとシステム全体の実行結果が各スレッドの処理の実行順序に依存する形になり、同じ入力を与えても、プログラム実行のたびに結果が変わる非決定的な動作となる。

ロック (lock)

マルチスレッド環境における排他制御にて各スレッドのアクセス順序を決定する。クリティカルセクションへのアクセス制限を行い、不整合な状態が起こらないよう制御する手法の一つ。このアクセス制限を課す動作を「ロックする」、「ロックを取得する」などと表現する。

デッドロック (deadlock)

排他制御によりロックされたクリティカルセクションに、他のユーザからアクセス要求が出された時、両者は互いに使用中のクリティカルセクションが解放されるのをブロック状態で待つという状況。この状態ではどのユーザも共有データの解放を待ったまま処理が進まずにプログラム停止状態となる。

アトミック性(Atomicity)

異なるスレッドが共通のデータにアクセスするような複数の操作を、同時に一つのスレッドだけが処理し途中で割り込まれることがなく、最後まで完了することを保証する性質。アトミックな操作のことをこれ以上分けることができないとして「不可分操作」と呼ぶ。アトミック性を保証できない場合でのマルチスレッド環境では競合が発生しうる。

可視性(Visibility)

複数のスレッドが共有する変数が、スレッド間でどのように見えるかを示す。あるスレッドが共有変数に書き込みをした場合、他のスレッドがその変数の値を読み込むときには、その書き込みが見えない事がある。可視性が保証するために、スレッド間での明示的な同期が必要になる。

5-2. 競合状態(race condition)

定義   複数スレッドでの処理が、クリティカルセクションに同時アクセスした場合に、データの不整合が起き、結果的にシステム停止など予期しない処理結果が生じてしまうこと。競合状態になるとシステム全体の実行結果が各スレッドの処理の実行順序に依存する形になり、同じ入力を与えても、プログラム実行のたびに結果が変わる非決定的な動作となる。

解説   競合状態を解防止するには、対象となるクリティカルセクションの独占権を保証すること。つまり適切な排他制御が必要となる。リソース独占のために、ロックという処理を行いアトミック性(後述)を保証する。非決定的な動作は非アトミックな操作と同義。ロックを正しく扱わないと、デッドロックを起こしえる。デッドロックは、お互いにロックされたリソースの解放を待ってしまい、処理が進まなくなってしまうこと。ロックを同じ順序で取得するように設計すれば、予防できると言われている。競合し整合性が失われたデータがDBなどにより永続化された場合、どのデータが間違っていて、いつどこで整合性が失われたのか調査するのは非常に難しい。

Twitter API の突然の仕様変更などでユーザーがサービスを使用できないみたいな混乱が多々あったのは記憶に新しいところ。そんな中で見かけたのだけど、その様な状態を予想して、細かい区間でログやキャッシュを残して後々サルベージしやすい様にして、ユーザがアクセスできなかった間の記録をサルベージしている企業もあったけどどんなアーキテクチャで実現してたんだろう。

5-3. ロック (lock)

定義   マルチスレッド環境における排他制御にて各スレッドのアクセス順序を決定する。クリティカルセクションへのアクセス制限を行い、不整合な状態が起こらないよう制御する手法の一つ。このアクセス制限を課す動作を「ロックする」、「ロックを取得する」などと表現する。

解説  あるスレッドがロックしたクリティカルセクションへは、基本的には他のスレッドによる利用は妨げられる。実際には、完全に利用をさせないロックは性能低下が著しいため、複数の主体が取得可能なロックや、他者の読み出しのみ許可するなど、複数のモード(レベル)のロックを用意し必要に応じて使い分ける。(ミューテックスセマフォ・共有ロック・占有ロックなど)

ロックは、スレッドがクリティカルセクションにアクセスする前にロックを取得し、アクセスが完了したらロックを解放することによって機能する。スレッドがクリティカルセクションをロック解除すると、他のスレッドがロックを取得できるようになる。これにより、複数スレッドがクリティカルセクションにアクセスしようとしても、一度に一つのスレッドしかアクセスできないようにすることができる。 

5-3-1. 簡易解説:ロック関連

単一ロック(Mutex)

定義   クリティカルセクションへのアクセスを保護するために使用される同期手法。クリティカルセクションを1つのスレッドのみがアクセスできるようにする。

解説   セマフォクリティカルセクションの数を制御するために使用され、ミューテックスクリティカルセクションへのアクセスを保護するために使用される。

ミューテックスは、ロックとアンロックの2つの基本操作があり、1つのスレッドがミューテックスをロックすると、他のスレッドはそのミューテックスをロックすることができない。ミューテックスを使用することにより、クリティカルセクションに対する同期が可能になり、競合状態やデータ競合などの問題を回避することができる。単一のスレッドしかロックを取得できないロック。

セマフォ(Semaphore)

定義   クリティカルセクションの数を制御するために使用される同期手法。クリティカルセクションを使用できるスレッドの数を制御するために使用。カウンティングセマフォとバイナリセマフォの2種類がある。

解説   セマフォクリティカルセクションの数を制御するために使用され、ミューテックスクリティカルセクションへのアクセスを保護するために使用される。

カウンティングセマフォは、特定のクリティカルセクションの利用可能な数を表す非負整数値であり、複数のスレッドやプロセスが同時にアクセスできる。初期値を3と設定すれば、三つのスレッドまでがクリティカルセクションにアクセス可能となり、4つ目のスレッドがアクセスしてきた場合、そのスレッドは順番待ちになる。バイナリセマフォは、利用可能なクリティカルセクションが1つしかない場合に使用される。0または1の値を取り、1の場合は利用可能であり、0の場合は利用不可である。バイナリセマフォは、ミューテックスとほぼ等価。

バイナリセマフォミューテックスの違い

参考:フリー百科事典『ウィキペディア(Wikipedia) :セマフォ

ミューテックスはリソースを排他的に使用する。一度に1つの実行単位だけがリソースを使用できることを意味します。 対してリソースへのアクセスを制限することは、複数の実行単位がリソースを使用できますが、一度に使用できる実行単位の数を制限することを意味します。

データベースは共有リソースと見なせます。ミューテックスを使用してデータベースを排他的にロックすると、データベースに同時にアクセスできるスレッドは1つだけです。セマフォを使用してデータベースへのアクセスを制限すると、データベースに同時にアクセスできるスレッドは、セマフォのカウンタ値によって決まります。たとえば、カウンタ値が3の場合、3つのスレッドが同時にデータベースにアクセスできます。

気をつけなくてはいけないのが、ミューテックスです。むやみやたらにミューテックスを使用すると、マルチスレッドの利点である効率性を大きく損なう可能性があります。スレッド数が多い処理に単一のスレッドしかアクセスを許さないのであれば、そこがボトルネックになってしまうからです。

読み取りロック(共有ロック) / 書き込みロック(専有ロック)

定義   共有データに対して、複数の読み取り専用アクセスを許可するが、共有データを変更する場合は、排他的なアクセス(一度に一つのスレッドのみ)を許可する。

解説   複数スレッドが共有データを同時に読み取り、書き込む場合、競合状態の発生が懸念される。読み取りロックは、複数スレッドが同時にデータを読み取ることは許可するが、データ変更は許可しないため、競合状態を回避する。書き込みロックは、データを変更するスレッドが存在する場合にのみロックを解除し、他スレッドが同時にデータを書き込むことを防ぐ。一般的な同期プリミティブ書き込み側がいつまでもロックを獲得できない事態を避けるために、書き込みが読み取りに優先するように実装することも考える。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

条件変数 (condition variable)

定義   状態が変化するまでスレッドをブロックする機能。スレッドが特定の条件を満たすまで待機する。

解説   あるスレッドがある変数の値を待つように設定された条件変数に対して待機することができる。別のスレッドがその変数の値を変更した場合、条件変数を通じて待機しているスレッドに通知する。これにより、スレッドが相互に連携して処理を行うことができ、競合状態を避けながら効率的にクリティカルセクションを共有できる。通常、ミューテックスセマフォなどの同期オブジェクトと併用して使用される。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

5-4. デッドロック(Deadlock)

定義   排他制御によりロックされたクリティカルセクションに、他のユーザからアクセス要求が出された時、両者は互いに使用中のクリティカルセクションが解放されるのをブロック状態で待つという状況。この状態ではどのユーザも共有データの解放を待ったまま処理が進まずに停止状態となる。

解説   複数のスレッドがロックを獲得しようと競合し、どのスレッドもロックを獲得できない状態。スレッドはロックを解放せず、プログラムが停止する。デッドロックの回避には『単一ロック(Mutex)』『ロック順序を守る』『タイムアウト機能』などがあるが、デッドロックが発生した場合に備えデッドロック検知・自動回復機能の実装も重要。

Deadlock_at_a_four-way-stop.gif

引用:フリー百科事典『ウィキペディア(Wikipedia)』 デッドロック

上記の図はスレッド・クリティカルセクション・ロックオブジェクト・デッドロックを示している。

  1. 移動する青玉
    • →スレッドもしくはプロセス(実行単位)
  2. 真ん中の大玉
  3. スレッドがクリティカルセクションに入るときに出てくる黄玉
    • →ロックオブジェクトとその取得
  4. ロックの取得が行われると他のスレッドはクリティカルセクションへのアクセスをブロックされる
  5. スレッドがクリティカルセクションから出ていく時に戻っていく黄玉
    • →ロックオブジェクトの解放
  6. ロックが解放されると、次のスレッドがロックを取得
  7. 最後

    • 四つのスレッドが同時にアクセスし、同時にロックを取得する 
    • デッドロック発生
  8. リソースの持ち合いによるデッドロック 複数のスレッドがお互いに必要なクリティカルセクションのロックを同時期に占有し、他方のクリティカルセクションの解放を待つ状態で、どちらのスレッドもクリティカルセクションを解放しないために前に進めなくなってしまうケース。上記のGifの状態。

基本的にデッドロッククリティカルセクション数が2以上の場合に発生する。クリティカルセクションが1の場合、セマフォ等はバイナリセマフォとなり、振る舞いはミューテックスと同じになるのでデッドロックは発生しない。ただし、ライブロックと言われる状態になりうる。デッドロックと似ているが、ライブロックはスレッドの実行は行われている。ただし、大きなパフォーマンス低下を引き起こす。ライブロックは、2人の歩行者が狭い道ですれ違おうとしている状況に例えることができる。歩行者は、お互いが相手の動きを予測して動こうとしますが、うまくいかず、何時間もすれ違えないという状況になる。

クリティカルセクション数を1にすることは、デッドロックを回避する根本的な解決方法であるが、その場合プログラムの並列性は著しく損なわれるため、現代のコンピュータープログラミングにおいて現実的な手段とは言えない。ミューテックスは現在使われることはほとんどない様ですが、概念として理解しておく必要がある。

5-4-1. デッドロックを回避する手段

デッドロックを発生させるには4つの条件が同時に満たされている必要があるため、いずれかの条件を1つでも崩すことができればデッドロックの発生を防ぐことができる。

  1. 相互排除(Mutual Exclusion): リソースが排他的に占有され、他のスレッドやプロセスなどの実行単位が同時にそのリソースを利用できない状態。デッドロックが発生するためには、複数のロックが存在し、それらのロックを獲得することができる状況が必要。
  2. 保持と待ち(Hold and Wait): 実行単位が少なくとも1つのリソースを占有したまま、他のリソースの解放を待っている状態。スレッドが少なくとも1つのロックを獲得した状態で、他のロックを待っている場合、デッドロックのリスクがある。
  3. 入れ子になった要求(Nesting of Requests): 実行単位がすでに占有しているリソースの解放を待ちながら、他のリソースを要求している状態。つまり、リソースの要求が入れ子になっている状態。スレッドが既に占有しているロックの解放を待ちながら、他のロックを要求している場合、デッドロックが発生する可能性がある。
  4. 循環待ち(Circular Wait): リソースの待ち合わせが循環的な関係を持っている状態。複数のスレッドやプロセスがリソースを循環的に要求し合っている状態。複数のスレッドやプロセスが異なる順番でロックを獲得しようとしており、循環的な依存関係が形成され、デッドロックが発生する可能性がある。

循環的な依存関係(Circular Dependency)

循環的な依存関係(Circular Dependency)は、複数の要素やエンティティが循環的に依存し合っている状態を指します。つまり、AがBに依存し、BがCに依存し、CがAに依存するといったように、循環的な結び付きが存在することを意味します。

引用元:Why Circular Dependencies Between Java Packages are Bad? image.png

例えば、AとBが互いに依存し合っている場合、Aの初期化にはBが必要であり、Bの初期化にはAが必要となるような関係です。このような場合、Aの初期化を待ってBを初期化する必要があり、一方でBの初期化も待ってAを初期化する必要があるため、どちらも進まずに相互に待ち合わせることになります。これが循環的な依存関係による問題の一例です。

循環的な依存関係は、ソフトウェア開発やデータベース設計などの領域でよく見られます。この問題を解決するためには、依存関係の再設計や解消が必要になります。例えば、依存関係の切断や抽象化、インターフェースの導入、依存性注入(Dependency Injection)の導入などが考えられます。これにより、循環的な依存関係を回避し、システムの正常な動作と進行を確保することができます。

上記を踏まえ、デッドロックを回避するには、以下の選択肢を状況によって使い分ける・組み合わせる必要がある。

  1. ロック順序の統一:複数のロックが必要な場合、特定の順序でロックを獲得することで、デッドロックを回避する。
  2. タイムアウト処理の導入:ロックの獲得が一定時間内に完了しなかった場合、ロックを開放し、処理を中断する。
  3. ロックの自動開放:あらかじめ決められた時間経過後に、ロックを自動的に開放する。
  4. ロックの共有:複数のスレッドが同時に読み込み処理を行う場合、排他制御をかけずに複数のスレッドが同時にアクセスできるようにすることで、デッドロックを回避する。
  5. リソース階層の確立:複数のリソースを使用する場合、リソースの階層を決め、上位リソースを先にロックすることで、デッドロックを回避する。

DBのトランザクションは上記の方法を複合的に使用した排他制御トランザクションは、複数の操作を一つの論理的な作業単位として扱うための仕組みであり、デッドロックの発生を回避するために排他制御が行われる。トランザクションは、原子性、一貫性、分離性、持続性の特性を備えなければならない。

トランザクションは、ロックを使用してクリティカルセクションへのアクセスを保護。ロックの順序、タイムアウト処理、ロックの自動開放、ロックの共有、リソース階層を使用して、デッドロックを回避する。だが、それでも完璧ではなく、デッドロックが発生する可能性はなくせない。デッドロックが発生した場合は、データベース管理者が介入してデッドロックを解除する必要がある。

その他の参考としての引用:
フリー百科事典『ウィキペディア(Wikipedia)』 Lock-freeとWait-freeアルゴリズム

マルチスレッドプログラミングにおいて古典的な手法は、共有リソースにアクセスするときはロックをかけることである。ミューテックスセマフォといった排他制御は、ソースコードにおいて共有リソースにアクセスする可能性のある領域(クリティカルセクション)を複数同時に実行しないようにすることで、共有メモリの構造を破壊しないようにする。もし、スレッドAが事前に獲得したロックを別のスレッドBが獲得しようとするときは、ロックが解放されるまでスレッドBの動作は停止する。

ロックの解放を待機するスレッドは、スリープやスピンといった手法で待機する。スリープ中はプロセッサを他のスレッドに空け渡すため、システム全体の負荷が下がるが、スリープの時間的な精度や分解能はオペレーティングシステムやプロセッサによって異なることがあり、またスリープから復帰する際に時間的オーバーヘッドが発生する。一方スピンによる待機(スピンロック)中は、スレッドはプロセッサを解放せず、システム全体に負荷をかけたままになる。

スレッドが停止することは多くの理由で望ましくない。まず、スレッドがブロックされている間は、そのスレッドは何もできない。そして、スレッドが優先順位の高い処理やリアルタイム処理を行っているならば、そのスレッドを停止することは望ましくない。また、複数のリソースにロックをかけることは、デッドロック、ライブロック、優先順位の逆転を起こすことがある。さらに、ロックを使うには、並列処理の機会を減らす粒度の粗い(すなわちクリティカルセクションが広い)ロックを選択するか、バグを生みやすく注意して設計しないといけない粒度の細かいロックを選択するかというトレードオフ問題を生む。

5-5. アトミック性(Atomicity)

定義   異なるスレッドが共通のデータにアクセスするような複数の操作を、同時に一つのスレッドだけが処理し途中で割り込まれることがなく、最後まで完了することを保証する性質。アトミックな操作のことをこれ以上分けることができないとして「不可分操作」と呼ぶ。アトミック性を保証できない場合でのマルチスレッド環境では競合が発生しうる。

解説   アトミックな操作とは、クリティカルセクションに対するマルチスレッド操作・複合アクション(インクリメントなど。後述)に対する適切は排他制御がなされている状態を指す。途中で別のプロセスやスレッドが割り込んできても、処理が途中で中断されたり、不正な状態になることがなく、データの整合性が保たれる必要がある。例えば、変数に対するインクリメント操作は、複数のスレッドが同時に実行しても、その結果が正しくなるようにアトミックに実行される必要がある。

アトミック性が保証されていない場合、一方のスレッドが変数を更新中に他方のスレッドが同じ変数にアクセスすると、変数の値が不正確な状態になり、意図しない結果が生じる可能性がある。Java ではsynchronizedキーワードやLockインタフェースなどの機能が提供されている。複数のスレッドからアクセスされる共有メモリ内の変数に対して、複数の操作がアトミックに実行されることが保証される。synchronizedにより排他制御が行われアトミック性が保証されるが、同時にsynchronizedは可視性との両方を確保する。

Java言語仕様は、単一の変数への読み込みと書き込みがアトミックであることを保証しているため、単純な変数の読み書きにsynchronizedを使う必要はない。

// 変数の書きこみはアトミック
int value = 1;

ただし、Javaでは、long型やdouble型の変数は64ビットのサイズ。32bitのアーキテクチャを持つシステムでも64bitの値を扱うことができるように、2つの32ビットの変数に分割する。そのため、原則としてアトミック性が保証されない。これらの型の変数に値を代入する場合、2つの32bit値に分割されてからメモリに格納されるため、複数のスレッドが同時にアクセスすると、意図しない結果が生じる可能性がある。このような場合、volatile修飾子を使用することで、他のスレッドからは64bitの1つの書き込みとして見えるようになりアトミックな動作を保証できる。

参考:JPCERT Coordination Center:「VNA05-J. 64ビット値の読み書きはアトミックに行う」

// 次の命令はアトミックではない。二つのバイト命令に変換される可能性がある。
long value = 1L;

// 次の命令はアトミック
volatile long value = 1L;

※「L」はlong型のリテラルであることを示すために使用される。Javaでは、整数リテラルの末尾にLまたはlを追加することにより、long型を表現する。Lを省略すると、Javaコンパイラは整数リテラルをint型として解釈する。

5-6. 可視性(Visibility)

定義   複数のスレッドが共有する変数が、スレッド間でどのように見えるかを示す。あるスレッドが共有変数に書き込みをした場合、他のスレッドがその変数の値を読み込むときには、その書き込みが見えない事がある。可視性が保証するために、スレッド間での明示的な同期が必要になる。

解説  1. volatilesynchronizedの軽量版 1. synchronizedはアトミック性・可視性を保証する 1. volatileは可視性のみを保証する 1. volatilesynchronizedより実行時のオーバーヘッドが少ない 1. volatilesynchronizedを使ってできることの一部しかできない

可視性の問題は、一方のスレッドが共有メモリ内の変数を更新しても、他方のスレッドからは更新後の値がすぐに見えない反映されないといった問題が発生すること。つまりそのスレッドは、共有変数の最新ではない値を得る可能性があるということ。最新の更新が反映された値を確実に得るためには、変数をvolatile宣言するか、変数に対する読み書きを同期(synchronizedなど)する必要がある。Javaでは、可視性の問題を解決するために、volatileもしくはsynchronizedキーワードが提供される。volatileとは揮発性という意味。

この可視性の問題は Java のメモリモデルの仕組みにより発生する。この辺りはリオーダーやらイントラスレッド・セマンティクスやらめちゃくちゃにややこしくて理解できなかったので、ざっくりと解説。

マルチスレッドの場合、それぞれのスレッドは独自のキャッシュメモリを持っている可能性がある。そのため、あるスレッドが共有変数を上書きしてもその結果はキャッシュだけに反映されてメインメモリには反映されない。他のスレッドからは結果をすぐに見られない状態になりうる。同じ共有変数にも関わらずプロセッサごとに異なる値に見えてしまい、スレッド間でデータの不整合が発生する。

volatileをつけた変数に対しての読み込みや書き込みは、常にメインメモリから行われるため、キャッシュに値が残っている場合でも最新の値が見えるようになる。

ただし、volatileだけを付けるケースは少ない上にかなり高度な理解が必要ようなる様です。詳細は以下を参照して下さい。ぶっちゃけよく解りません。 - JPCERT Coordination Center:VNA00-J. 共有プリミティブ型変数の可視性を確保する

使い所としては、複数スレッド間で可視性を確保する場面。しかし複合操作におけるアトミック性を保証するものではないため 複合操作を排他的に行いたい場合には使うことはできないらしい。 - その変数への書き込みが、その変数の現在の値に依存しない - その変数が、他の変数との不等式に使われない - インクリメント演算には使えない - Compare and Assignment には使えない

上記二点を満たすケースの実装としてvolatile の利用パターンは以下があるらしい。もはや良くわかりません。今はこういうのがあるんだなーって感じです。

  • パターン1:ステータスフラグとしての利用
  • パターン2:1度だけ安全に公開する
  • パターン3:独立した観測結果の公開
  • パターン4:volatile bean パターン
  • パターン5:安価な読み書きロック(高度な利用例)

参考
『Java の理論と実践: volatile を扱う』を読んで
[Java] volatile 変数

6. これまでの要約

  1. プログラム実行には実行内容をメモリ上に展開する必要がある

    1. ほとんどのパソコンはノイマン型コンピュータ
    2. HDDなどの補助記憶装置へのアクセスはオーバーヘッドが大きいのでさせない
    3. そのため、これらのコンピュータはプログラムを実行するために、メモリにプログラムを展開する必要がある。
    4. メモリは、プログラム実行中に処理されるデータの一時的な記憶領域。
    5. プロセッサは、プログラムの実行に必要な情報を得るためにメモリにアクセスする。
  2. Javaには、プログラムが実行される際に使用される複数のメモリ領域がある。

  3. 主なメモリ領域は、スタック領域、ヒープ領域、static領域、コンスタントプール。これらのメモリの管理方法やデータの使用目的によって、メモリの空間が定義されている。 static 領域のメモリ空間

  4. static領域の特徴と使い分けは以下の通り

    • static領域は、クラススコープ内で static キーワード付きで変数宣言を行う。プログラムのどの部分からでも参照することが可能。
    • static領域に格納されるデータは、プログラムの開始から終了まで有効。インスタンス化は必要ない。
    • static領域には、プログラム全体で共有する変数や定数を格納する。
      staticメソッドは、インスタンスを生成しなくても呼び出すことができるため、ユーティリティメソッドやファクトリーメソッドなどに利用される。しかし、static変数やメソッドを多用すると、プログラムの拡張性低下・メモリの無駄遣いになるため、必要最小限に使用することが望ましい。
  5. static メソッドの使い所

    • ユーティリティメソッド
      • インスタンスを必要としない共通の処理を実行する場合。例えば、Math クラスの abs メソッドは、引数に渡された数値の絶対値を返す。このような処理は、インスタンス化する必要がないため、static メソッドとして実装される。
    • ファクトリメソッド
      • インスタンスを作成するためのメソッドを static メソッドとして定義する。例えば、Java の Collections クラスには、空のリストやイテレーターなどを作成するための static メソッドが用意されている。
  6. static フィールドの使い所

    • 定数
      • static フィールドを定数として定義することができます。例えば、Java の Math クラスには、πの値を表す static フィールドが定義されている。
    • 共有データ
      • クラスの全てのインスタンスで共有されるデータを static フィールドとして定義することができる。環境変数などが定義されたファイルやインスタンスなどの様に一度決まれば変更されない、もしくは変更される場合にそれを参照する対象全てが同期して欲しいものなど。
  7. static 領域のフィールド・メソッドはどこからでもアクセスできる

    • 他のクラスからアクセスする際には、以下のようにクラス名を指定して参照可能。オブジェクトをインスタンス化する必要はない。MyClass.increment();
  8. static メソッドやフィールドは、オブジェクトの状態に依存しない

    • static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されていてインスタンス化の必要はない。
    • インスタンスごとに異なる値を持つインスタンスフィールド。異なる値に依存する、つまり、状態に依存するのがインスタンス
  9. staticメソッドの注意点

    • 参照共有・アンスレッドセーフ問題
      • static メソッドは、クラスのすべてのインスタンスで共有されるため、意図しない変更が発生する可能性がある。
      • static メソッドは、他のスレッドから同時に呼び出される可能性があるため、スレッドセーフではない。
  10. プロセス(Process)

    • プログラムの実行単位。OSによって管理される実行中のプログラムのインスタンス
    • 親プロセスからフォークされ、子プロセスがあり、プログラムコード、データ、スタックなどの情報を保持する仮想アドレス空間を持つ image.png
    • メモリ空間やCPUなどのシステムリソースを割り当てられている
    • 複数のスレッドを含めることができる
    • 独立しており、他のプロセスのメモリに直接アクセスできない
      • プロセス間でデータを共有するには、共有メモリやパイプ、ソケット通信などのメカニズムを使用する必要がある
    • データ不整合や競合状態が発生しないように、適切な同期処理が必要
  11. スレッド(Thread)

    • プロセスと同じくプログラムの実行の単位で「軽量なプロセス」。プロセス内で複数のスレッドが同時に実行できる
      • スレッドはプロセスに含まれる
    • 同じプロセスに属するスレッドはメモリ空間を共有するため、スレッド間通信が容易。
      • スレッドはプロセス内で通信することができ、並行処理に使用できる。
    • 同じデータを複数のスレッドが同時に書き換えることによる不整合に注意する必要がある。
    • スレッドセーフかどうかわからない場合は、同期・排他制御、スレッド間データ共有を防ぐか不変オブジェクトの使用などを行う必要がある。
  12. プロセスとスレッドの違い

    • スレッドはプロセスの内部での話であり、プロセスがスレッドを含む。
    • プロセスは複数のスレッドを持つことができ、複数のスレッドが1つのプロセス内で実行されることをマルチスレッドと呼ぶ。
    • プロセスとスレッドの主な違いは、プロセスは独立した実行単位であり、スレッドはプロセス内の依存関係のある実行単位である。
  13. スレッドセーフ(Thread Safe)

    • 複数のスレッドからメソッド、フィールド、または共有データにアクセスしても問題がないことを指す。スレッドセーフではない操作とは、非アトミックな操作を含む。
      • または複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。
    • スレッドセーフでないリソースを複数スレッドからアクセスする場合には、同期・排他制御を行わなくてはならない。
      • 一般的には、リソースのクラスをスレッドセーフにする方法が望ましいとされる。
  14. クリティカルセクション・同期制御・排他制御

    • クリティカルセクションは、複数の処理が同時に実行されると競合状態を起こす単一の共有データであり、コードセクションを指す。
    • 競合状態とは、複数のスレッドが同じデータまたはリソースに同時にアクセスしようとする状況。競合状態は、同じ入力を与えてもプログラム実行のたびに結果が変わってしまう非決定的な動作であり、データの破損またはシステム停止につながる可能性がある。
    • 同期制御は複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。
    • 排他制御を行うためには、ロックを取得し解放する必要がある。ただし、デッドロックに注意しなければならず、Javaでは synchronized や ReentrantLock などの方法がある。また、共有データへのアクセスには、スレッド間の排他制御を行い、アトミック性を確保する必要がある。
    • ロックは、クリティカルセクションへのアクセスを制限する排他制御の一種。スレッドがクリティカルセクションにアクセスする前にロックを取得し、アクセスが完了したらロックを解放する。スレッドがクリティカルセクションをロック解除すると、他のスレッドがロックを取得できるようになる。
  15. アトミック性(Atomicity)

    • アトミックな操作とは、クリティカルセクションに対するマルチスレッド操作・複合アクション(インクリメントなど。後述)に対する適切は排他制御がなされている状態を指す。
    • アトミック性とは、、複数のスレッドが共有する変数に、1つのスレッドが操作を開始してから完了するまで他の操作をブロックする性質。これは、複数のスレッドが同じ変数にアクセスするときに重要。不可分操作と呼ぶ。
    • アトミック性が保証されていない場合は、複数のスレッドが同時にクリティカルセクションにアクセスした場合に競合しデータ不整合を引き起こす可能性がある。
    • Java では、synchronized キーワードや Lock インタフェースなどの機能が提供されている。
  16. 可視性(Visibility)

    • 視性とは、複数のスレッドが共有する変数に、1つのスレッドが値を書き込んだ後、他のスレッドがその値をすぐに読み取ることができることを保証する性質。
    • volatile修飾子は、可視性を保証するために使用される。volatile 修飾子が付いた変数は、常にメインメモリから読み書きされるため、キャッシュに値が残っていても最新の値が見えるようになる。これにより、複数のスレッド間での可視性が保証される。
    • ただし、volatile修飾子は複合操作のアトミック性を保証するものではないため、複合操作を排他的に行いたい場合には使えない。

続きは

  • 『後編:synchronized の使い方と注意点』

static と synchronized から始めるマルチスレッド入門 〜後編〜 - よしたろうブログ

前編は

  • 『前編:static とメモリ管理』

static と synchronized から始めるマルチスレッド入門 〜前編〜 - よしたろうブログ