
Java超|スレッドの同期と排他制御
仲間が同時に動くほど、共有する記録は慎重に守る必要がある。スレッドの同期と排他制御を理解すると、複数の処理が同じデータを扱っても、正しい結果を保てるようになります。
スレッドは、複数の処理を並行して進められる便利なしくみです。
これまで、Thread クラスを継承する方法や、Runnable インターフェイスを実装する方法を使って、main とは別の処理の流れを作る方法を学んできました。
ドラゴンボール風にたとえると、戦士本部の任務で、BulmaSupport が修行場の準備を進めているあいだに、GokuWarrior が重力修行を進め、VegetaWarrior が戦闘フォームを整えるような流れです。
複数の戦士が同時に動けるようになると、任務全体を効率よく進められます。
しかし、スレッドは増やせば増やすほど単純に便利になる、というものではありません。
複数のスレッドが、それぞれ完全に独立した処理だけをしているなら、大きな問題は起きにくいです。
たとえば、GokuWarrior スレッドは修行メッセージを表示するだけ、VegetaWarrior スレッドは別のメッセージを表示するだけ、というように、互いに共有するデータがない場合です。
ところが、複数のスレッドが 同じデータ を読み書きし始めると、注意が必要になります。
たとえば、戦士本部に「共通の修行記録帳」が1冊だけあるとします。
GokuWarrior も VegetaWarrior も、自分の修行ポイントをその記録帳に加算します。
本来なら、次の流れで正しく合計が増えていくはずです。
| 手順 | 内容 |
|---|---|
| 1 | 今の合計ポイントを見る |
| 2 | 自分の修行ポイントを足す |
| 3 | 新しい合計ポイントを書き戻す |
しかし、2人が同じ記録帳をほぼ同時に見てしまうと、古い合計値をもとに別々に計算してしまうことがあります。
その結果、片方の加算が反映されず、合計ポイントが本来より小さくなる可能性があります。
このような問題を防ぐために必要になるのが、同期 と 排他制御 です。
| 用語 | 意味 |
|---|---|
| 同期 | 複数スレッドの処理タイミングを整えるしくみ |
| 排他制御 | 同じ共有資源に同時に1つのスレッドだけが入れるようにするしくみ |
| synchronized | Javaで排他制御を行うために使える指定 |
この記事では、複数スレッドで同じデータを扱うと何が起こるのか、なぜ synchronized が必要なのか、どの範囲を守る必要があるのかを、Sample7.java を使ってていねいに整理していきます。
複数スレッドで同じデータを扱うと何が起こるのか
スレッドが別々の仕事をしているだけなら、問題は比較的起きにくいです。
たとえば、次のような場合です。
| スレッド | 処理 |
|---|---|
| GokuWarrior スレッド | GokuWarrior が修行メニューを表示する |
| VegetaWarrior スレッド | VegetaWarrior が戦闘フォーム確認を表示する |
| main スレッド | BulmaSupport が準備を表示する |
この場合、それぞれが別々のメッセージを出しているだけなので、表示順は混ざるかもしれませんが、共有データの値が壊れるような問題は起きにくいです。
しかし、次のような場合は注意が必要です。
| 状況 | 問題になりやすい理由 |
|---|---|
| 同じ合計値を複数スレッドが更新する | 古い値をもとに計算する可能性がある |
| 同じ在庫数を複数スレッドが減らす | 在庫数が実際とずれる可能性がある |
| 同じファイルに複数スレッドが書き込む | 内容が混ざったり上書きされたりする可能性がある |
| 同じオブジェクトの状態を複数スレッドが変更する | 状態の整合性が崩れる可能性がある |
ここで大切なのは、値を増やす処理が見た目では単純でも、内部では複数の段階に分かれているということです。
たとえば、合計ポイントに50を加える処理を考えます。
total = total + 50;この1行は、見た目には1つの処理に見えます。
しかし、考え方としては次の流れに分けられます。
| 段階 | 内容 |
|---|---|
| 1 | total の現在値を読む |
| 2 | 読んだ値に 50 を足す |
| 3 | 計算結果を total に書き戻す |
この途中で別のスレッドが同じ total を読んでしまうと、古い値をもとに計算してしまうことがあります。
ドラゴンボール風にたとえると、GokuWarrior が修行記録帳を開いて現在の合計を見る。
まだ新しい合計を書き終わっていないのに、VegetaWarrior も同じ記録帳を見てしまう。
すると、2人とも同じ古い合計を見て、それぞれ修行ポイントを足してしまいます。
その結果、どちらかの記録が上書きされたようになり、正しい合計になりません。
共有資源とは何か
共有資源とは、複数のスレッドから共通で利用されるデータや対象のことです。
今回の例では、修行ポイントの合計を表す total が共有資源です。
| 共有資源の例 | 具体例 |
|---|---|
| フィールド | 合計値、残高、在庫数、カウンタ |
| ファイル | ログファイル、設定ファイル、記録ファイル |
| データベース | 同じレコード、同じテーブル |
| オブジェクト | 共通設定、共有リスト、共通カウンタ |
共有資源があるとき、複数スレッドが同時に読み書きすると、値の食い違いが起きる可能性があります。
特に、次のような処理は注意が必要です。
| 処理 | 注意点 |
|---|---|
| 読む | まだ更新前の古い値を読んでしまうことがある |
| 計算する | 古い値をもとに計算してしまうことがある |
| 書き戻す | 別スレッドの更新を上書きしてしまうことがある |
ドラゴンボール風に言えば、共通の修行記録帳は1冊しかありません。
GokuWarrior も VegetaWarrior もその帳面を使います。
だからこそ、誰かが書いている途中に、別の戦士が同じページを開いて書き込むと、記録が乱れてしまいます。
図:共有データを同時に更新すると矛盾が起きる

この図が示していること
この図は、GokuWarrior スレッドと VegetaWarrior スレッドが、同じ total をほぼ同時に読んでしまう状態を表しています。
本来なら、GokuWarrior が50を足し、VegetaWarrior も50を足せば、合計は100になるはずです。
しかし、2人が同じ古い値 0 を読んでから、それぞれ 50 を計算して書き戻すと、結果が 50 のままになる可能性があります。
| 図の要素 | 意味 |
|---|---|
| 修行記録帳 total | 複数スレッドが共有するデータ |
| GokuWarrior スレッド | total を読み、50を足して書き戻す |
| VegetaWarrior スレッド | 同じ total を読み、50を足して書き戻す |
| 赤い矢印 | 同時アクセスによる衝突 |
| 結果が50になる可能性 | 片方の更新が失われる状態 |
この図から分かることは、問題は足し算そのものではなく、読み取りから書き戻しまでの一連の処理が途中で割り込まれてしまうことにある、という点です。
矛盾が起こる例を見る
ここでは、共通の修行ポイントを管理するクラスと、そのポイントを増やす戦士スレッドを用意します。
GokuWarrior と VegetaWarrior が、同じ TrainingCenter オブジェクトを共有し、それぞれ修行ポイントを加算します。
ファイル名:Sample7.java
// 修行ポイントを管理するクラス
class TrainingCenter
{
private int total = 0;
public void addPoint(int p)
{
int tmp = total;
System.out.println("現在の合計ポイントは" + tmp + "です。");
System.out.println(p + "ポイント増やします。");
tmp = tmp + p;
System.out.println("合計ポイントを" + tmp + "にします。");
total = tmp;
}
}
// 修行する戦士クラス
class WarriorWorker extends Thread
{
private TrainingCenter center;
private String name;
public WarriorWorker(TrainingCenter c, String nm)
{
center = c;
name = nm;
}
public void run()
{
for(int i = 0; i < 3; i++) {
System.out.println(name + "が修行ポイントを加算します。");
center.addPoint(50);
}
}
}
class Sample7
{
public static void main(String[] args)
{
TrainingCenter tc = new TrainingCenter();
WarriorWorker worker1 = new WarriorWorker(tc, "GokuWarrior");
worker1.start();
WarriorWorker worker2 = new WarriorWorker(tc, "VegetaWarrior");
worker2.start();
}
}プログラムの登場人物
このプログラムには、3つのクラスが登場します。
| クラス | 役割 | ドラゴンボール風のイメージ |
|---|---|---|
| TrainingCenter | 修行ポイントの合計を管理する | 戦士本部の共通修行記録帳 |
| WarriorWorker | 修行ポイントを加算するスレッド | 修行成果を記録する戦士 |
| Sample7 | 実行用クラス | 本部で任務を開始する指令役 |
TrainingCenter には、修行ポイントの合計を表す total フィールドがあります。
private int total = 0;この total が、今回の共有資源です。
GokuWarrior スレッドも VegetaWarrior スレッドも、同じ TrainingCenter オブジェクト tc を共有します。
TrainingCenter tc = new TrainingCenter();
WarriorWorker worker1 = new WarriorWorker(tc, "GokuWarrior");
WarriorWorker worker2 = new WarriorWorker(tc, "VegetaWarrior");つまり、2人は同じ修行記録帳にポイントを書き込むことになります。
addPoint メソッドの処理
TrainingCenter クラスの addPoint メソッドを見てみましょう。
public void addPoint(int p)
{
int tmp = total;
System.out.println("現在の合計ポイントは" + tmp + "です。");
System.out.println(p + "ポイント増やします。");
tmp = tmp + p;
System.out.println("合計ポイントを" + tmp + "にします。");
total = tmp;
}このメソッドは、次の流れで合計ポイントを更新しています。
| 順番 | 処理 |
|---|---|
| 1 | total の値を tmp に取り出す |
| 2 | 現在の合計を表示する |
| 3 | 加算するポイントを表示する |
| 4 | tmp に p を足す |
| 5 | 新しい合計を表示する |
| 6 | tmp を total に戻す |
1人だけが使うなら、この流れで問題ありません。
しかし、複数スレッドが同時に addPoint に入ると、途中で別スレッドが同じ total を読んでしまう可能性があります。
本来どうなるはずなのか
このプログラムでは、GokuWarrior と VegetaWarrior が、それぞれ3回ずつ 50ポイント を加算します。
つまり、計算上は次のようになります。
| 戦士 | 加算回数 | 1回あたり | 合計 |
|---|---|---|---|
| GokuWarrior | 3回 | 50 | 150 |
| VegetaWarrior | 3回 | 50 | 150 |
| 全体 | 6回 | 50 | 300 |
本来なら、最終的な合計ポイントは 300 になるはずです。
しかし、addPoint メソッドが synchronized で守られていない場合、実行タイミングによっては 300 にならない可能性があります。
なぜ正しい合計にならないことがあるのか
理由は、addPoint の処理が途中で割り込まれる可能性があるからです。
たとえば、次のような流れが起きることがあります。
| 時点 | GokuWarrior スレッド | VegetaWarrior スレッド | total |
|---|---|---|---|
| 1 | total を読む → 0 | 0 | |
| 2 | total を読む → 0 | 0 | |
| 3 | 0 + 50 = 50 | 0 | |
| 4 | 0 + 50 = 50 | 0 | |
| 5 | total に 50 を書く | 50 | |
| 6 | total に 50 を書く | 50 |
本来は、GokuWarrior が50を足し、VegetaWarrior がさらに50を足して、合計100になってほしい場面です。
しかし、2人とも最初に 0 を読んでしまったため、どちらも 50 を書き戻します。
その結果、合計は 100 ではなく 50 のままになる可能性があります。
このように、片方の更新が消えてしまうような状態を、更新の競合として考えることができます。
処理の途中に割り込まれることが問題
ここで大切なのは、addPoint 全体を1つのまとまりとして完了させたい、という点です。
addPoint の中では、次の流れがあります。
int tmp = total;
tmp = tmp + p;
total = tmp;この一連の処理は、途中で別スレッドに入られると困ります。
ドラゴンボール風に言えば、修行記録帳への書き込みは、次の3つが終わって初めて1回分の記録です。
| 手順 | 記録係の作業 |
|---|---|
| 1 | 今の合計を見る |
| 2 | 自分のポイントを足す |
| 3 | 新しい合計を書く |
GokuWarrior がこの作業をしている途中に、VegetaWarrior が同じページを見てしまうと、古い合計をもとに計算してしまいます。
だから、1人が addPoint を実行している間は、もう1人は待つ必要があります。
このように、共有資源を扱う一連の処理を守るために使うのが、同期と排他制御です。
同期とは何か
同期とは、複数スレッドの処理のタイミングを調整して、順番を守らせるしくみです。
今回の例では、次のような状態を作りたいわけです。
| 状況 | 望ましい動き |
|---|---|
| GokuWarrior が addPoint を実行中 | VegetaWarrior は addPoint に入らず待つ |
| VegetaWarrior が addPoint を実行中 | GokuWarrior は addPoint に入らず待つ |
| addPoint が終わった | 次のスレッドが addPoint に入れる |
つまり、修行記録帳に書き込むときは、1人ずつ順番に作業してほしいのです。
ドラゴンボール風に言えば、記録帳の前に「記入中は次の戦士は待機」という札を立てるようなものです。
これによって、途中の値を別の戦士が見てしまうことを防げます。
排他制御とは何か
排他制御とは、ある共有資源に対して、同時に1つのスレッドしか入れないようにする考え方です。
同期は広い意味で「タイミングを整えること」です。
その中でも、排他制御は「同時に使わせないこと」に焦点を当てた考え方です。
| 用語 | 内容 |
|---|---|
| 同期 | スレッドどうしの処理タイミングを整える |
| 排他制御 | 共有資源に同時に1つのスレッドだけが入れるようにする |
| synchronized | 排他制御を行うためにメソッドなどに付ける指定 |
今回の addPoint では、total を扱う一連の処理を同時に実行させないことが大切です。
つまり、addPoint 全体を排他的に守る必要があります。
synchronized を使うとどうなるのか
Javaでは、メソッドに synchronized を付けることで、そのメソッドを同時に複数スレッドが実行できないようにできます。
addPoint に synchronized を付けると、次のようになります。
public synchronized void addPoint(int p)
{
int tmp = total;
System.out.println("現在の合計ポイントは" + tmp + "です。");
System.out.println(p + "ポイント増やします。");
tmp = tmp + p;
System.out.println("合計ポイントを" + tmp + "にします。");
total = tmp;
}これにより、あるスレッドが addPoint を実行している間、同じオブジェクトに対して別のスレッドは addPoint に入れなくなります。
| synchronized なし | synchronized あり |
|---|---|
| GokuWarrior と VegetaWarrior が同時に addPoint に入る可能性がある | 1人ずつしか addPoint に入れない |
| 古い値を同時に読んでしまう可能性がある | 前の更新が終わってから次が読む |
| 合計がずれる可能性がある | 合計が正しく積み上がりやすい |
ドラゴンボール風に言えば、synchronized は修行記録帳の前に置く「記入中の戦士以外は入室禁止」の結界のようなものです。
GokuWarrior が書いている間、VegetaWarrior は待ちます。
GokuWarrior が書き終えて記録帳を閉じたあと、VegetaWarrior が入って続きを書きます。
図:synchronized による排他制御

この図が示していること
この図は、synchronized を付けた addPoint に、同時に1つのスレッドしか入れないことを表しています。
GokuWarrior スレッドが addPoint を実行している間、VegetaWarrior スレッドは待機します。
GokuWarrior の処理が終わり、total への書き戻しまで完了してから、VegetaWarrior が addPoint に入ります。
| 図の要素 | 意味 |
|---|---|
| synchronized addPoint | 排他的に守られたメソッド |
| 青い結界 | 同時侵入を防ぐ排他制御 |
| GokuWarrior スレッド | 先に addPoint に入っているスレッド |
| VegetaWarrior スレッド | GokuWarrior の処理完了を待つスレッド |
| 更新後の total | 次のスレッドが読む正しい値 |
この図から分かることは、synchronized は処理を単に遅くするためのものではなく、共有データの整合性を守るために、処理の順番を整えるしくみだということです。
synchronized を付けた処理の確認
addPoint に synchronized を付けると、共有データ total を扱う処理を1人ずつ行えるようになります。
ファイル名:Sample7.java(変更後)
// 修行ポイントを管理するクラス
class TrainingCenter
{
private int total = 0;
public synchronized void addPoint(int p)
{
int tmp = total;
System.out.println("現在の合計ポイントは" + tmp + "です。");
System.out.println(p + "ポイント増やします。");
tmp = tmp + p;
System.out.println("合計ポイントを" + tmp + "にします。");
total = tmp;
}
}
// 修行する戦士クラス
class WarriorWorker extends Thread
{
private TrainingCenter center;
private String name;
public WarriorWorker(TrainingCenter c, String nm)
{
center = c;
name = nm;
}
public void run()
{
for(int i = 0; i < 3; i++) {
System.out.println(name + "が修行ポイントを加算します。");
center.addPoint(50);
}
}
}
class Sample7
{
public static void main(String[] args)
{
TrainingCenter tc = new TrainingCenter();
WarriorWorker worker1 = new WarriorWorker(tc, "GokuWarrior");
worker1.start();
WarriorWorker worker2 = new WarriorWorker(tc, "VegetaWarrior");
worker2.start();
}
}synchronized を付けたときの流れ
synchronized を付けると、addPoint の流れは次のように守られます。
| 順番 | 処理 |
|---|---|
| 1 | GokuWarrior が addPoint に入る |
| 2 | GokuWarrior が total を読む |
| 3 | GokuWarrior が 50 を足す |
| 4 | GokuWarrior が total に書き戻す |
| 5 | GokuWarrior が addPoint を出る |
| 6 | VegetaWarrior が addPoint に入る |
| 7 | VegetaWarrior が更新後の total を読む |
| 8 | VegetaWarrior が 50 を足す |
| 9 | VegetaWarrior が total に書き戻す |
このように、addPoint の処理が1回ずつきちんと完了してから次のスレッドが入ります。
そのため、合計ポイントが正しく積み上がりやすくなります。
なぜ addPoint 全体を守る必要があるのか
今回のポイントは、total を読む部分だけを守ればよいわけではない、ということです。
addPoint では、次の流れ全体が1つの意味を持っています。
| 処理 | 役割 |
|---|---|
| total を tmp に読む | 現在の合計を確認する |
| tmp に p を足す | 新しい合計を計算する |
| total に tmp を戻す | 合計を更新する |
この3つは、途中で分断されてはいけない一連の処理です。
途中で別スレッドが入ると、古い total を読んでしまう可能性があります。
だから、addPoint 全体に synchronized を付けて、読み取りから書き戻しまでをまとめて守ります。
ドラゴンボール風に言えば、修行記録帳への書き込みは、ページを見るだけでは終わりません。
現在の合計を見る。
自分の点数を足す。
新しい合計を書き込む。
ここまで終わって、1回分の記録です。
だから、途中で別の戦士が入ってきてはいけません。
synchronized で改善されること
synchronized を付けると、次のような改善が期待できます。
| 状態 | 結果 |
|---|---|
| synchronized なし | 値の食い違いが起きる可能性がある |
| synchronized あり | 共有データの更新が順番に行われやすい |
今回の例では、GokuWarrior と VegetaWarrior がそれぞれ3回ずつ50ポイントを加算します。
synchronized によって addPoint が1回ずつ守られれば、合計は期待どおり 300 に近づきます。
| 戦士 | 加算回数 | 1回あたり | 合計 |
|---|---|---|---|
| GokuWarrior | 3回 | 50 | 150 |
| VegetaWarrior | 3回 | 50 | 150 |
| 全体 | 6回 | 50 | 300 |
ただし、表示の順番そのものは固定されません。
synchronized が守るのは、addPoint の中へ同時に入らせないことです。
GokuWarrior の表示が先に出るか、VegetaWarrior の表示が先に出るかまでは、スレッドの実行タイミングによって変わります。
| synchronized が守ること | synchronized が固定しないこと |
|---|---|
| addPoint の同時実行を防ぐ | スレッド全体の表示順 |
| total の更新の整合性を守る | GokuWarrior と VegetaWarrior の実行順すべて |
| 共有データの更新を1回ずつ完了させる | どちらが先に何回動くか |
実務で同期が重要になる場面
同期と排他制御は、学習用の加算処理だけの話ではありません。
実際の開発でも、共有データを複数スレッドで扱う場面はたくさんあります。
| 場面 | 同期しないと起こりうる問題 |
|---|---|
| 在庫数の更新 | 同時注文で在庫数がずれる |
| 口座残高の更新 | 入金や出金の結果が狂う |
| ログイン回数の集計 | カウント漏れが起きる |
| ファイル更新 | 内容の上書きや混在が起きる |
| データベース更新 | 一部の更新が失われる |
| 共有カウンタ | 実際より少ない値になる |
たとえば、在庫が1つしかない商品に対して、2つの注文処理が同時に入った場合を考えます。
どちらの処理も「在庫が1つある」と読んでしまうと、2人に売れてしまう可能性があります。
これは、修行記録帳の total と同じ構造です。
| 修行記録の例 | 実務の例 |
|---|---|
| total を読む | 在庫数を読む |
| 50を足す | 1つ減らす |
| total に戻す | 在庫数を書き戻す |
| 古い値を同時に読むと合計がずれる | 古い在庫数を同時に読むと販売数がずれる |
このように、共有データの読み取り、計算、書き戻しがある場面では、同期がとても重要になります。
図:共有資源を守る考え方

この図が示していること
この図は、複数スレッドが共有資源を扱うときに、同期と排他制御で守る必要があることを表しています。
共有資源には、修行ポイントの合計だけでなく、在庫数、口座残高、ログファイル、共有カウンタなどがあります。
これらを複数スレッドが同時に更新すると、値がずれたり、更新が失われたりする可能性があります。
| 図の要素 | 意味 |
|---|---|
| 共有資源 | 複数スレッドから使われるデータや対象 |
| synchronized の結界 | 同時アクセスを防ぐ排他制御 |
| GokuWarrior スレッド | 共有資源を更新したいスレッド |
| VegetaWarrior スレッド | 同じ共有資源を更新したい別スレッド |
| 待機 | ほかのスレッドの処理完了を待つ状態 |
この図から分かることは、スレッドを安全に使うには、処理の流れを増やすだけでなく、共有資源をどう守るかまで考える必要があるということです。
synchronized はどこに付けるべきか
synchronized は、共有データを扱う一連の処理に付ける必要があります。
今回なら、total を扱う addPoint メソッドです。
public synchronized void addPoint(int p)ここに synchronized を付ける理由は、addPoint の中に total の読み取り、計算、書き戻しがすべて含まれているからです。
| 守るべき処理 | 理由 |
|---|---|
| total を読む | 古い値を同時に読ませないため |
| tmp に p を足す | 読んだ値をもとに計算するため |
| total に戻す | 更新結果を確実に反映するため |
一部だけを守っても、別の部分で割り込まれれば矛盾が起きる可能性があります。
そのため、意味のある処理のまとまり全体を守ることが大切です。
ドラゴンボール風に言えば、修行記録帳を開く瞬間だけ守っても不十分です。
合計を見る。
点数を足す。
新しい合計を書く。
この一連の記録作業が終わるまで、ほかの戦士が割り込まないようにする必要があります。
synchronized を使うときの注意
synchronized は便利ですが、何でもかんでも付ければよいわけではありません。
排他制御を行うと、同時に入れるスレッド数を制限することになります。
そのため、必要な範囲を正しく見極めることが大切です。
| 注意点 | 内容 |
|---|---|
| 必要な共有データを守る | 共有される値や資源を中心に考える |
| 守る範囲を広げすぎない | 不必要に待ち時間が増える可能性がある |
| 一連の更新処理はまとめて守る | 読む、計算する、書き戻すを分断しない |
| 表示順の固定とは別問題 | synchronized は主に共有データの整合性を守る |
今回の addPoint のように、共有フィールド total を更新する処理では、synchronized を付ける意味が分かりやすいです。
しかし、単にメッセージを表示するだけの処理など、共有データを壊す可能性が低い場所まで過剰に守る必要はありません。
同期と排他制御の関係
同期と排他制御は、似た場面で出てくる言葉です。
学習段階では、次のように整理すると分かりやすいです。
| 用語 | 役割 | ドラゴンボール風のイメージ |
|---|---|---|
| 同期 | スレッドどうしのタイミングを整える | 戦士たちの行動順をそろえる |
| 排他制御 | 同じ場所に同時に入れないようにする | 記録帳への記入を1人ずつにする |
| synchronized | 排他制御を実現するための指定 | 記録帳を守る結界 |
同期は広い意味で、スレッドのタイミングを調整することです。
排他制御は、その中でも「同時に使わせない」という考え方です。
今回の synchronized は、addPoint に同時に1つのスレッドしか入れないようにするため、排他制御の例として理解できます。
スレッドの同期と排他制御で押さえたいこと
スレッドの同期と排他制御を学ぶときは、次の点を押さえると理解しやすくなります。
| ポイント | 内容 |
|---|---|
| 複数スレッドは便利 | 複数の処理を並行して進められる |
| 共有データには注意が必要 | 同じ値を同時に更新すると矛盾が起きる可能性がある |
| 問題の原因 | 読む、計算する、書き戻すの途中で別スレッドが割り込むこと |
| 共有資源 | 複数スレッドから使われるデータや対象 |
| 同期 | スレッドどうしのタイミングを整えるしくみ |
| 排他制御 | 同じ共有資源へ同時に入れないようにするしくみ |
| synchronized | メソッドなどを排他的に実行させるための指定 |
| 守る対象 | 共有データを扱う一連の処理全体 |
スレッドは、ただ複数の流れを作るだけでは安全に使えるとは限りません。
複数の流れが同じデータを触るときは、そのデータをどう守るかまで考える必要があります。
ドラゴンボール風に言えば、複数の戦士が同じ修行記録帳に書き込むなら、1人ずつ順番に書くルールが必要です。
GokuWarrior が記録している間は、VegetaWarrior は待つ。
GokuWarrior が書き終わったら、VegetaWarrior が更新後の合計を見て書く。
この順番を守ることで、修行ポイントの合計が正しく積み上がります。
Javaでは、そのための基本的な方法として synchronized を使えます。
この考え方が分かると、スレッドは「同時に動く便利なしくみ」だけではなく、「共有データの正しさを守りながら動かす必要があるしくみ」として理解できるようになります。
