SynchronizedはJava言語に組み込まれた同期メカニズムであり、コードのクリティカルセクションへの排他的アクセスを提供します。Oracle, 2024によると、synchronized修飾子は、マークされたメソッドまたはブロックを特定の時点で1つのスレッドのみが実行できることを保証します。このメカニズムはモニターに基づいています。モニターはオペレーティングシステムの基本概念であり、あらゆる複雑度のマルチスレッドアプリケーションの正確な動作を保証します。
主要ポイント
SynchronizedはJavaのキーワードで、一度に1つのスレッドのみが保護されたコードセクションを実行することを保証し、並行アクセス時のデータ破損を防ぎます。Javaの最初のバージョンで登場し、あらゆるレベルの開発者にとってスレッドセーフティを確保する最も簡単な方法であり続けています。
synchronized修飾子は2つのタスクを解決します:相互排他と変更の可視性。スレッドがsynchronizedブロックを終了すると、すべての変更は同じオブジェクトで同期されたブロックに入る他のスレッドに確実に可視になります。
Synchronizedはメソッド全体、またはモニターオブジェクトを指定して任意のコードブロックに適用できます。どちらの場合も、JVMはバイトコードレベルでmonitorenterおよびmonitorexit命令を挿入します。
同期なしのマルチスレッドアプリケーションでは、2つのスレッドが同時に同じデータを変更するとレースコンディションが発生し、予測不可能な結果をもたらします。Synchronizedはこの問題に対処するJava初の主要ツールとなり、どの開発者でも利用可能なシンプルな宣言的構文を提供します。
Synchronizedメカニズムは、モニターの概念に基づいています。モニターはすべてのJavaオブジェクトに組み込まれた高レベルの同期プリミティブです。モニターは、synchronizedブロックがオブジェクト上で最初に使用された時点で関連付けられます。
Javaのすべてのオブジェクトには関連付けられたモニターがあります。スレッドがsynchronizedブロックに入ると、オブジェクトのモニターを取得します。モニターが既に別のスレッドによって保持されている場合、スレッドは解放されるまでブロックされます。バイトコードでは、これはmonitorenterとmonitorexitの命令ペアに対応します。
JVMは複数のレベルを通じてsynchronizedを最適化します:単一スレッドアクセス用のbiased locking(偏向ロック)、低競合用のlightweight locking(軽量ロック)、OSが関与する高競合用のheavyweight locking(重量ロック)。これらのレベルはコードを変更せずにパフォーマンスを向上させます。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronizedはhappens-before関係を確立します:synchronizedブロックを終了する前のスレッド内のすべてのアクションは、同じオブジェクトで同期されたブロックに入った後の別のスレッドに可視になります。これにより、相互排他だけでなく、すべてのスレッドのデータ整合性も保証されます。
Javaはsynchronizedを適用する2つの方法を提供します:メソッドレベルとブロックレベルです。どちらを選択するかは、パフォーマンスと同期の粒度に影響します。
メソッドにsynchronized修飾子を付けると、自動的に現在のインスタンス(インスタンスメソッドの場合)またはClassオブジェクト(静的メソッドの場合)で同期されます。これは相互排他を確保する最も簡単な方法ですが、クリティカルセクションがメソッドのごく一部のみを構成し、残りのコードに同期が必要ない場合には過剰になることがよくあります。
Synchronizedブロックは正確な制御を提供します:モニターオブジェクトを指定し、コードの必要な部分のみを同期し、メソッドの残りをロックの外に置きます。これによりモニター保持時間が最小化され、マルチスレッド環境でのアプリケーション全体のパフォーマンスが向上します。他のスレッドはモニターの解放を待たずに無関係なコードを並行して実行できます。
class DataProcessor {
private final Object lock = new Object();
public void process() {
// クリティカルセクション外のコード - 同期なし
prepareData()
synchronized (lock) {
// このブロックのみ保護されています
updateSharedState()
}
// ロックなしで続行
cleanup()
}
}
| 基準 | Synchronizedメソッド | Synchronizedブロック |
|---|---|---|
| モニター | this(インスタンス)またはClass | 任意のオブジェクト |
| 粒度 | メソッド全体 | 必要なコードのみ |
| 可読性 | 高い | 中程度 |
| パフォーマンス | 大きなメソッドでは低い | 小さなクリティカルセクションでは高い |
Android開発では、synchronizedはSharedPreferences、データベースアクセス、UIコンポーネントの保護に広く使用されています。ただし、メインスレッドでの使用はインターフェースのフリーズリスクのため強く推奨されません。
AndroidのSharedPreferencesは基本的なスレッドセーフティを提供しますが、Editorを介して複数のスレッドから編集する場合、外部同期が必要になることがあります。別のロックオブジェクトを使用したsynchronizedブロックが変更の整合性を保証します。
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
Androidでのsynchronizedの主な制限はスレッドブロッキングです。Mutexを使用するコルーチンとは異なり、synchronizedはシステムスレッド全体をブロックします。メインスレッドでは、これがANRを引き起こします。最新のAndroid開発では、synchronizedをコルーチン(suspend Mutex)またはアトミック型(AtomicInteger)に置き換えることが推奨されています。
最新のJavaとKotlinはsynchronizedの代替手段をいくつか提供しており、それぞれが同じ問題をより少ない制限またはより良いパフォーマンスで解決します。
Lockインターフェースは、ReentrantLockおよびReadWriteLockの実装により、タイムアウト、割り込み可能な待機、複数のConditionキューを提供します。synchronizedよりも柔軟ですが、finallyでの明示的な解放が必要であり、unlockを忘れるとエラーのリスクが高まります。
AtomicInteger、AtomicLong、AtomicReferenceなどのクラスは、CAS(Compare-And-Swap)に基づくLock-Freeアルゴリズムを使用します。これらは中程度の競合シナリオでsynchronizedよりも大幅に高速です。スレッドをブロックせず、OSカーネルのコンテキストスイッチを必要とせずに楽観的再試行を実行するためです。
ThreadLocalは代替アプローチを提供します:各ThreadLocal変数は単一のスレッド内で分離されており、読み取りと書き込みに同期を必要としません。これにより、スレッド間で共有すべきでないデータに対するsynchronizedの必要性が完全になくなります。ThreadLocalはフレームワーク(Spring、Hibernate)でトランザクションコンテキストとセッションの保存に積極的に使用されています。
Android向けKotlinプロジェクトでは、synchronizedの代替としてkotlinx.coroutinesのMutexがあります。これはオペレーティングシステムのスレッドをブロックせず、ロックが解放されるまでコルーチンを一時停止します。これによりプールスレッドの効率的な使用が可能になり、リソース解放の長時間待機中にANRを回避できます。
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
Synchronizedのパフォーマンスは最近のJavaバージョンで大幅に変化しました。以前は“重い”メカニズムと考えられていましたが、最新のJVMは高度なJITコンパイラ最適化によりほとんどのオーバーヘッドを排除しました。実行時に仮想マシンがどのように同期コードを高速化するかを詳しく見てみましょう。
JVMのJITコンパイラはいくつかの最適化を適用します:biased lockingはロックが常に同じスレッドによって取得される場合に同期を排除します;lock coarseningは隣接するsynchronizedブロックを1つに統合します;lock eliminationはオブジェクトが1つのスレッドからのみアクセス可能な場合に同期を削除します。これらの最適化により、低競合時にはsynchronizedは実質的にコストフリーになります。
JVMは各オブジェクトの競合レベルを決定します:競合がない場合はbiased lockingが有効になり、2番目のスレッドが現れるとロックはスピン待機の lightweightモードに移行し、長時間の待機時にのみシステムミューテックスを使用するheavyweightにエスカレーションします。このエスカレーションは自動的に発生し、開発者が手動で戦略を選択する必要はありません。
最新のベンチマーク(Java 17+)では、synchronizedは低および中程度の競合でReentrantLockと同等のパフォーマンスを示します。高競合時には、タイムアウトと割り込みをサポートするより効率的な待機キューによりLockが優位になる場合があります。競合が一定である高負荷システムでは、fairモードのReentrantLockがより予測可能な動作を提供します。
アトミッククラス(AtomicInteger、AtomicReference)は、CASベースのLock-Free実装により、単純なカウンターとフラグに対して最速のままです。これらはスレッドをまったくブロックしません。競合が発生した場合、操作はループ内で単純に再試行されます。これにより、4〜8スレッドでのカウンターインクリメント操作において、synchronizedと比較して3〜5倍のパフォーマンス向上が得られます。
よくある質問
Synchronizedは相互排他と可視性の両方を提供します。Volatileは変更の可視性のみを保証します。volatile変数への書き込みはすべてのスレッドに可視ですが、同時変更を防ぐことはできません。つまり、レースコンディションから保護しません。
はい、異なるモニター順序でのネストされた同期によりデッドロックが発生する可能性があります。例えば、あるスレッドがsynchronized(a) { synchronized(b) }を呼び出し、別のスレッドがsynchronized(b) { synchronized(a) }を呼び出します。ネストされたsynchronizedブロックを避けるか、一貫したモニター順序を固定してください。
モニターは各Javaオブジェクトに関連付けられた同期メカニズムです。そのオブジェクト上で1つのスレッドのみがsynchronizedコードを実行することを保証します。モニターにはロック、待機キュー、およびwait/notifyによる通知を待つスレッドのプールが含まれます。
最新のJavaバージョン(17+)では、synchronizedはJIT最適化(biased locking、lock coarsening)のおかげでパフォーマンスにおいてLockに劣りません。Lockが好まれるのは速度ではなく、タイムアウト、割り込み可能な待機、複数のConditionキューといった追加機能のためです。
静的synchronizedメソッドは、インスタンスではなく指定されたクラスのClassオブジェクトのモニターを使用します。これは、同期がクラスのすべてのインスタンスに適用されることを意味します。非静的メソッドと静的synchronizedメソッドは異なるモニターを使用し、互いにブロックしません。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。