synchronized は何を「約束」してくれるのか
synchronized は一言でいうと、
「同じ場所を同時に触ろうとする複数スレッドを、順番待ちにさせる仕組み」です。
もっと具体的に言うと、あるオブジェクト(ロック)に対して
「今はこのスレッドが作業中だから、他のスレッドは終わるまで待ってね」
という“鍵”をかけるイメージです。
これによって、複数スレッドが同じ変数を同時に書き換えて、
値が壊れる(レースコンディション)ことを防ぎます。
なぜ synchronized が必要になるのか(問題のイメージ)
共有変数を複数スレッドで触ると何が起きるか
とても単純なクラスを考えます。
class Counter {
private int value = 0;
void increment() {
value++;
}
int get() {
return value;
}
}
Javaこれを複数スレッドから同時に呼ぶとどうなるか。
value++ は実は「読み取り → 足し算 → 書き戻し」という複数ステップの処理です。
二つのスレッドが同時に increment() を呼ぶと、
片方が読み取った値と、もう片方が読み取った値が同じになってしまい、
結果として「2 回インクリメントしたのに 1 しか増えていない」
ということが普通に起こります。
これがレースコンディションです。synchronized は、まさにこの「同時に触られると困る場所」に鍵をかけるためのキーワードです。
synchronized の基本構文(メソッドとブロック)
メソッドに synchronized を付ける
先ほどの Counter を安全にする一番シンプルな方法は、
メソッドに synchronized を付けることです。
class Counter {
private int value = 0;
synchronized void increment() {
value++;
}
synchronized int get() {
return value;
}
}
Javaこうすると、
同じ Counter インスタンスに対してincrement() や get() を呼ぶ複数スレッドは、
必ず「一つずつ順番に」メソッドを実行するようになります。
「同じインスタンスの synchronized メソッド同士は、同時に実行されない」
という約束ができるわけです。
ブロックに synchronized を付ける
メソッド全体ではなく、一部だけを守りたいときは、synchronized ブロックを使います。
class Counter {
private int value = 0;
void increment() {
synchronized (this) {
value++;
}
}
int get() {
synchronized (this) {
return value;
}
}
}
Javasynchronized (this) は
「このインスタンス(this)を鍵としてロックする」
という意味です。
メソッドに synchronized を付けた場合も、
内部的には「そのインスタンスをロックしている」と考えて構いません。
synchronized の中身:モニタロック(オブジェクトにぶら下がった鍵)
「オブジェクトごとに鍵が 1 個ある」とイメージする
Java の synchronized は、
「オブジェクトにぶら下がっているモニタロック」を使います。
synchronized (obj) と書いたとき、
その obj に対して
「今このロックを持っているスレッドは誰か?」
という情報が JVM 内部で管理されます。
あるスレッドが synchronized (obj) に入るとき、
まだ誰もそのロックを持っていなければ、
そのスレッドがロックを取得して中に入ります。
すでに別のスレッドがロックを持っていれば、
ロックが解放されるまで待たされます。
synchronized ブロックやメソッドを抜けるときに、
ロックは自動的に解放されます。
インスタンスメソッドとクラスメソッドの違い
インスタンスメソッドに synchronized を付けると、
「そのインスタンス(this)のロック」を使います。
synchronized void foo() { ... } // synchronized (this) と同じイメージ
Java静的メソッドに synchronized を付けると、
「そのクラスオブジェクトのロック」を使います。
static synchronized void bar() { ... } // synchronized (クラスオブジェクト) と同じイメージ
Javaつまり、
同じインスタンスに対する synchronized インスタンスメソッド同士は同時に実行されない。
同じクラスに対する synchronized static メソッド同士も同時に実行されない。
どのロックを共有しているかを意識すると、
「どこまでが同時実行禁止になるか」が見えてきます。
synchronized の重要な性質:再入可能(reentrant)とメモリ可視性
再入可能(同じスレッドなら何度ロックしてもよい)
synchronized のロックは「再入可能(reentrant)」です。
同じスレッドが、同じロックを何度取っても構いません。
class Example {
synchronized void outer() {
inner(); // ここでも this のロックを取りに行く
}
synchronized void inner() {
// ...
}
}
Javaouter() を呼ぶと、
まず outer で this のロックを取得します。
その中で inner() を呼ぶと、
再び this のロックを取りに行きますが、
「すでにこのスレッドが持っているロックなので OK」と判断され、
そのまま入れます。
これがもし再入不可だったら、
自分自身の中で自分を呼び出した瞬間にデッドロックしてしまいます。
再入可能であることは、実用上とても重要です。
メモリ可視性:ロックの出入りが「見える化」の境界になる
synchronized は「順番待ち」だけでなく、
メモリの見え方 にも影響します。
あるスレッドが synchronized ブロックの中で変数を書き換えたとき、
そのブロックを抜ける(ロックを解放する)までに行った書き込みは、
同じロックを後から取得した別のスレッドから必ず見える、
という保証があります。
ざっくり言うと、
ロックを「取る」ときと「解放するとき」が、
メモリの「同期ポイント」になっている、ということです。
これによって、
「片方のスレッドが書いた値が、
もう片方のスレッドからいつまでも見えない」
といった不思議な現象を防いでいます。
具体例:synchronized なしとありの違いを感じる
synchronized なしの危険なカウンタ
class UnsafeCounter {
private int value = 0;
void increment() {
value++;
}
int get() {
return value;
}
}
Javaこれを 1000 個のスレッドから同時に increment() させると、
最終的な値は 1000 にならないことがよくあります。
synchronized ありの安全なカウンタ
class SafeCounter {
private int value = 0;
synchronized void increment() {
value++;
}
synchronized int get() {
return value;
}
}
Javaこちらは、同じインスタンスに対してなら、
何スレッドから同時に叩いても、
最終的な値は必ず「呼び出した回数」と一致します。
「同時に触らせない」ことと「メモリの見え方を揃える」ことをsynchronized が両方やってくれているからです。
synchronized を使うときに意識してほしいこと
どのオブジェクトをロックに使っているかを常に意識する
synchronized (lock) と書いたとき、
「この lock を共有しているコードは全部、同時に実行されない」
という関係が生まれます。
違うオブジェクトをロックに使っていると、synchronized を付けても全く関係なく同時実行されてしまいます。
「この共有データを守るためのロックはどれか?」
「どのメソッドが同じロックを使っているか?」
を頭の中で整理しながら synchronized を付けるのが、とても大事です。
ロックの範囲を広げすぎない(けど狭めすぎない)
synchronized を付けると、その部分は必ず順番待ちになります。
守りたいのは「共有データを触る瞬間」だけなのに、
その前後の重い処理まで全部 synchronized に入れてしまうと、
スレッドが無駄に待たされて性能が落ちます。
逆に、共有データを触る部分を分割してしまうと、
その間に別スレッドが割り込んできて、
整合性が崩れることがあります。
「どこからどこまでを“一つのまとまった操作”として守るべきか」
を丁寧に考えることが、synchronized 設計の肝です。
まとめ:synchronized を自分の言葉で説明するなら
あなたの言葉で synchronized を説明すると、こうなります。
「synchronized は、オブジェクトにぶら下がった“ロック”を使って、
複数スレッドが同じ場所を同時に触らないようにする仕組み。
同じロックを使うコードは必ず順番待ちになり、
その出入りがメモリの同期ポイントにもなる。
インスタンスメソッドに付ければそのインスタンス単位、
static メソッドに付ければクラス単位でロックされる。
どのオブジェクトをロックに使うか、どこまでの範囲を守るかを意識して設計することが、
安全でかつ性能の良い並行プログラムを書く鍵になる。」
