Lockは、マルチスレッドアプリケーションにおいてコードのクリティカルセクションへの排他的アクセスを提供する同期メカニズムです。Oracle(2024年)によると、Lockインターフェースは従来のsynchronizedブロックと比較してより柔軟な同期制御を提供し、タイムアウト付きの取得試行や複数の待機キューのサポートを含みます。
重要ポイント
Lockはjava.util.concurrent.locksパッケージのインターフェースで、データアクセスを同期するための明示的なロックおよびアンロック操作を提供します。synchronizedとは異なり、Lockは開発者にロックメカニズムの完全な制御を提供します。
LockインターフェースはJava 5で組み込みのsynchronizedメカニズムの代替として導入されました。主なメソッドはlock、unlock、tryLock、lockInterruptiblyです。ロックはマルチスレッド環境での安全なデータアクセスを整理し、レースコンディションやデータ破損を防ぎます。
synchronizedに対するLockの主な利点は柔軟性です。開発者はタイムアウト付きでロックの取得を試みたり、ブロックせずに利用可能性を確認したり、異なる優先順位で複数の待機キューを整理したりできます。
Java 5でLockインターフェースが登場する以前は、唯一の同期方法はsynchronizedでしたが、タイムアウトの欠如、割り込み可能な待機の欠如、単一キューといった制限がありました。Doug Leaはjava.util.concurrentパッケージを設計し、Lockを基本的な構成要素として含めました。
ロックは内部状態フラグと待機キューを通じてアクセスを管理します。スレッドがlock()を呼び出すと、メカニズムはロックが空いているかどうかを確認し、取得するか、解放されるまでスレッドをキューに入れます。
ロックの中心にはアトミックな比較交換(CAS)操作があります。lock()が呼び出されると、スレッドはアトミックにビジーフラグを設定しようとします。フラグがすでに設定されている場合、スレッドはブロックされます。unlock()でフラグがクリアされ、待機中のスレッドが1つ起床されます。
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// クリティカルセクション
println("スレッド ${Thread.currentThread().name} が動作中")
} finally {
lock.unlock()
}
}
ReentrantLockは内部的に双方向リンクリスト(CLHロックキュー)を使用し、各待機スレッドはノードで表されます。ロックが解放されると、キューの先頭ノードが起床されます。公平モードはFIFO順序を保証しますが、不公平モードはスループットを向上させるために新しいスレッドが待機中のスレッドより先にロックを取得することを許可します。
現代のJavaエコシステムには、特定のシナリオに最適化された複数のロック実装があります。適切なロックを選択することは、マルチスレッドアプリケーションのパフォーマンスと信頼性に直接影響します。
ReentrantLockは基本的で最もよく使用されるLock実装です。同じスレッドによる再取得をサポートします。スレッドがすでにロックを保持している場合、lock()を再度呼び出してもブロックされません。これにより再帰呼び出しでのデッドロックを防ぎます。
ReadWriteLockはロックを読み取りと書き込みの2つのモードに分離します。複数のスレッドが同時に読み取りロックを保持できますが、書き込みには排他的アクセスが必要です。これにより、頻繁な読み取りと稀な書き込み時のパフォーマンスが大幅に向上します。
StampedLockはJava 8で導入された最新の実装です。書き込み、読み取り、楽観的読み取りの3つのモードをサポートします。楽観的読み取りは他のスレッドをブロックせず、読み取り後にデータを検証するため、ReadWriteLockと比較して10〜20%のパフォーマンス向上を提供します。
| ロック | Javaバージョン | モード | パフォーマンス |
|---|---|---|---|
| ReentrantLock | Java 5 | 排他的 | 高い |
| ReadWriteLock | Java 5 | 読み取り + 書き込み | 中程度 |
| StampedLock | Java 8 | 読み取り + 書き込み + 楽観的 | 非常に高い |
ReentrantLockは最も人気のあるLock実装であり、synchronizedでは利用できないいくつかの機能を提供します。その特徴を理解することは効果的なマルチスレッド作業に不可欠です。
ReentrantLockのコンストラクタはfairパラメータを受け入れます。trueの場合、ロックはFIFO順序を保証します。falseの場合、新しいスレッドが待機中のスレッドより先にロックを取得できる場合があります。公平モードはスターべーションを防ぎますが、キュー維持のオーバーヘッドのためスループットが10〜20%低下します。
synchronizedとは異なり、ReentrantLockはタイムアウト付きのtryLockをサポートします。指定された時間内にロックを取得できない場合、スレッドは無期限にブロックされるのではなく実行を継続します。lockInterruptiblyメソッドはThread.interrupt()を介して待機中のスレッドを割り込むことを可能にします。
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("ロックを取得しました")
} finally {
lock.unlock()
}
} else {
println("ロックの取得に失敗しました")
}
}
ReentrantLockはnewCondition()メソッドを通じて複数の条件変数をサポートします。各Conditionには独自の待機キューがあり、複雑な起床シナリオを可能にします。await()およびsignal()メソッドはsynchronizedブロックのwait()およびnotify()を置き換えましたが、複数キューのサポートを備えています。
ReadWriteLockとStampedLockは、読み取りが書き込みより優勢な場合のアクセスの最適化を扱います。これらは、書き込みよりも読み取りが頻繁に発生するシナリオでReentrantLockよりも大幅に効率的です。
ReadWriteLockインターフェースにはreadLock()とwriteLock()の2つのメソッドがあります。読み取りロックは複数のスレッドが同時に保持できますが、書き込みロックは排他的です。典型的な例はスレッドセーフなキャッシュです。多くのスレッドがデータを読み取り、1つのスレッドだけが定期的に更新します。
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLockは第3のモード「tryOptimisticRead」を追加します。このモードは他のスレッドをブロックせず、状態のスタンプを記録するだけです。読み取り後、開発者はvalidate(stamp)を呼び出して読み取り中にデータが変更されていないか確認します。データが変更された場合、操作を再試行する必要があります。
モバイルアプリケーションでは、ロックを使用してスレッド間の共有データへのアクセスを調整します。ただし、デバイスのリソースが限られており、UIの応答性を維持する必要があるため、その使用には特別な注意が必要です。
Androidでは、ReentrantLockはRoom、キャッシュ、ファイルを操作する際に有用です。重要な注意点:メインスレッドでロックを取得しないでください。非同期コードの場合は、kotlinx.coroutinesのコルーチンとMutexが推奨されます。これらはスレッドをブロックする代わりにコルーチンを一時停止します。
iOSでは、標準のNSLockはあまり使用されません。開発者はバリアフラグ付きのDispatchQueueやos_unfair_lockを好みます。Swift 5.7+はアクターを通じて最新の同期メカニズムを提供し、状態を自動的に保護します。
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
デッドロックを回避するには、プロジェクト全体で一貫したロック順序に従ってください。長時間のブロッキングが可能な場合は、lock()の代わりにタイムアウト付きのtryLockを使用してください。従来のロックの代わりにロックフリーアルゴリズム(AtomicReference、ConcurrentHashMap)の使用を検討してください。
Lockの使用には、デッドロックとパフォーマンス低下を防ぐための規律とルールの順守が必要です。これらのプラクティスは、java.util.concurrentパッケージの20年にわたる使用を通じてJavaコミュニティによって開発されました。
最も重要なパターンはfinallyでのロックです。クリティカルセクションが正常に完了するか例外をスローするかに関わらず、ロックは解放されなければなりません。これにより、1つのエラーが原因で他のスレッドが永久にブロックされないことが保証されます。Kotlinでは、このパターンはwithLock拡張関数によってエレガントに解決されます。
クリティカルセクションは可能な限り短くする必要があります。ロック内でI/O、ネットワークリクエスト、長時間の計算を決して実行しないでください。サーバーからデータを読み取る必要がある場合は、最初にデータを取得し、共有状態を更新するためだけにロックを取得してください。これにより競合が減少し、システムのスループットが向上します。
複数のロックを扱う際のデッドロックを防ぐには、プロジェクト全体でグローバルなロック順序を確立してください。最初にlockA、次にlockBが取得される場合、逆の順序はコードレビュールールによって禁止されるべきです。自動検証にはSpotBugsやIntelliJ Inspectionsなどの静的アナライザーを使用してください。
よくある質問
Lockはタイムアウトと割り込み可能な待機をサポートする明示的なインターフェースです。synchronizedは自動的にモニターを取得および解放しますが、tryLock、lockInterruptibly、複数のConditionsを使用することはできません。Lockはより柔軟ですが、finallyでの手動解放が必要です。
フェアロックはFIFO順序を保証します。最も長く待機しているスレッドが最初にロックを取得します。アンフェアロックは待機中のスレッドより先に新しいスレッドにアクセスを許可することがあり、スループットは向上しますが、待機中のスレッドのスターべーションを引き起こす可能性があります。
すべてのロックを取得する固定順序に従い、無条件のlock()の代わりにタイムアウト付きのtryLockを使用し、同時に保持するロックの数を最小限にしてください。ロックフリーデータ構造の使用もデッドロックのリスクを減らします。
ConditionはLockに対するwait/notifyの類似物であり、複数の独立した待機キューを可能にします。各newCondition()呼び出しは個別のキューを作成し、synchronizedの単一キューと比較してスレッドの起床をより正確に制御できます。
コルーチンを使用するAndroidの場合は、kotlinx.coroutinesのMutexを使用してください。スレッドをブロックする代わりにコルーチンを一時停止します。Swift 5.7+を使用するiOSの場合は、状態アクセスを自動的に同期するアクターが推奨されます。ReentrantLockはレガシーコードと低レベルシナリオに残してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。