Lockとは、ロックの種類と同期における使用法

著者: IT Sectr 公開日: 2026-03-19 読了時間: 8 分

Lockは、マルチスレッドアプリケーションにおいてコードのクリティカルセクションへの排他的アクセスを提供する同期メカニズムです。Oracle(2024年)によると、Lockインターフェースは従来のsynchronizedブロックと比較してより柔軟な同期制御を提供し、タイムアウト付きの取得試行や複数の待機キューのサポートを含みます。

重要ポイント

  • LockはJavaで明示的にロックを管理するためのインターフェースです。
  • ReentrantLockは、同じスレッドによる再取得をサポートする基本的な実装です。
  • ReadWriteLockはパフォーマンス向上のため読み取りロックと書き込みロックを分離します。
  • Deadlockは複数のロックを同時に使用する際の主なリスクです。
  • synchronizedとは異なり、Lockはタイムアウトと割り込み可能な待機をサポートします。

Lockとは?

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つ起床されます。

kotlin
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

ReentrantLockは基本的で最もよく使用されるLock実装です。同じスレッドによる再取得をサポートします。スレッドがすでにロックを保持している場合、lock()を再度呼び出してもブロックされません。これにより再帰呼び出しでのデッドロックを防ぎます。

ReentrantReadWriteLock

ReadWriteLockはロックを読み取りと書き込みの2つのモードに分離します。複数のスレッドが同時に読み取りロックを保持できますが、書き込みには排他的アクセスが必要です。これにより、頻繁な読み取りと稀な書き込み時のパフォーマンスが大幅に向上します。

StampedLock

StampedLockはJava 8で導入された最新の実装です。書き込み、読み取り、楽観的読み取りの3つのモードをサポートします。楽観的読み取りは他のスレッドをブロックせず、読み取り後にデータを検証するため、ReadWriteLockと比較して10〜20%のパフォーマンス向上を提供します。

ロックJavaバージョンモードパフォーマンス
ReentrantLockJava 5排他的高い
ReadWriteLockJava 5読み取り + 書き込み中程度
StampedLockJava 8読み取り + 書き込み + 楽観的非常に高い

ReentrantLockとその特徴

ReentrantLockは最も人気のあるLock実装であり、synchronizedでは利用できないいくつかの機能を提供します。その特徴を理解することは効果的なマルチスレッド作業に不可欠です。

ロックの公平性(fairness)

ReentrantLockのコンストラクタはfairパラメータを受け入れます。trueの場合、ロックはFIFO順序を保証します。falseの場合、新しいスレッドが待機中のスレッドより先にロックを取得できる場合があります。公平モードはスターべーションを防ぎますが、キュー維持のオーバーヘッドのためスループットが10〜20%低下します。

タイムアウトと割り込み可能な待機

synchronizedとは異なり、ReentrantLockはタイムアウト付きのtryLockをサポートします。指定された時間内にロックを取得できない場合、スレッドは無期限にブロックされるのではなく実行を継続します。lockInterruptiblyメソッドはThread.interrupt()を介して待機中のスレッドを割り込むことを可能にします。

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("ロックを取得しました")
        } finally {
            lock.unlock()
        }
    } else {
        println("ロックの取得に失敗しました")
    }
}

条件(Conditions)

ReentrantLockはnewCondition()メソッドを通じて複数の条件変数をサポートします。各Conditionには独自の待機キューがあり、複雑な起床シナリオを可能にします。await()およびsignal()メソッドはsynchronizedブロックのwait()およびnotify()を置き換えましたが、複数キューのサポートを備えています。

ReadWriteLockとStampedLock

ReadWriteLockとStampedLockは、読み取りが書き込みより優勢な場合のアクセスの最適化を扱います。これらは、書き込みよりも読み取りが頻繁に発生するシナリオでReentrantLockよりも大幅に効率的です。

ReadWriteLockの実践

ReadWriteLockインターフェースにはreadLock()とwriteLock()の2つのメソッドがあります。読み取りロックは複数のスレッドが同時に保持できますが、書き込みロックは排他的です。典型的な例はスレッドセーフなキャッシュです。多くのスレッドがデータを読み取り、1つのスレッドだけが定期的に更新します。

kotlin
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と楽観的読み取り

StampedLockは第3のモード「tryOptimisticRead」を追加します。このモードは他のスレッドをブロックせず、状態のスタンプを記録するだけです。読み取り後、開発者はvalidate(stamp)を呼び出して読み取り中にデータが変更されていないか確認します。データが変更された場合、操作を再試行する必要があります。

モバイル開発におけるロック

モバイルアプリケーションでは、ロックを使用してスレッド間の共有データへのアクセスを調整します。ただし、デバイスのリソースが限られており、UIの応答性を維持する必要があるため、その使用には特別な注意が必要です。

Android(Kotlin)でのロック

Androidでは、ReentrantLockはRoom、キャッシュ、ファイルを操作する際に有用です。重要な注意点:メインスレッドでロックを取得しないでください。非同期コードの場合は、kotlinx.coroutinesのコルーチンとMutexが推奨されます。これらはスレッドをブロックする代わりにコルーチンを一時停止します。

iOS(Swift)でのロック

iOSでは、標準のNSLockはあまり使用されません。開発者はバリアフラグ付きのDispatchQueueやos_unfair_lockを好みます。Swift 5.7+はアクターを通じて最新の同期メカニズムを提供し、状態を自動的に保護します。

swift
import Foundation

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }
}

デッドロック防止の推奨事項

デッドロックを回避するには、プロジェクト全体で一貫したロック順序に従ってください。長時間のブロッキングが可能な場合は、lock()の代わりにタイムアウト付きのtryLockを使用してください。従来のロックの代わりにロックフリーアルゴリズム(AtomicReference、ConcurrentHashMap)の使用を検討してください。

Lockを使用する際のベストプラクティス

Lockの使用には、デッドロックとパフォーマンス低下を防ぐための規律とルールの順守が必要です。これらのプラクティスは、java.util.concurrentパッケージの20年にわたる使用を通じてJavaコミュニティによって開発されました。

finallyでの解放

最も重要なパターンはfinallyでのロックです。クリティカルセクションが正常に完了するか例外をスローするかに関わらず、ロックは解放されなければなりません。これにより、1つのエラーが原因で他のスレッドが永久にブロックされないことが保証されます。Kotlinでは、このパターンはwithLock拡張関数によってエレガントに解決されます。

保持時間の最小化

クリティカルセクションは可能な限り短くする必要があります。ロック内でI/O、ネットワークリクエスト、長時間の計算を決して実行しないでください。サーバーからデータを読み取る必要がある場合は、最初にデータを取得し、共有状態を更新するためだけにロックを取得してください。これにより競合が減少し、システムのスループットが向上します。

一貫したロック順序

複数のロックを扱う際のデッドロックを防ぐには、プロジェクト全体でグローバルなロック順序を確立してください。最初にlockA、次にlockBが取得される場合、逆の順序はコードレビュールールによって禁止されるべきです。自動検証にはSpotBugsやIntelliJ Inspectionsなどの静的アナライザーを使用してください。

よくある質問

Lockとsynchronizedの違いは何ですか?

Lockはタイムアウトと割り込み可能な待機をサポートする明示的なインターフェースです。synchronizedは自動的にモニターを取得および解放しますが、tryLock、lockInterruptibly、複数のConditionsを使用することはできません。Lockはより柔軟ですが、finallyでの手動解放が必要です。

フェアロック(fair lock)とは何ですか?

フェアロックはFIFO順序を保証します。最も長く待機しているスレッドが最初にロックを取得します。アンフェアロックは待機中のスレッドより先に新しいスレッドにアクセスを許可することがあり、スループットは向上しますが、待機中のスレッドのスターべーションを引き起こす可能性があります。

Lock使用時にデッドロックを回避するには?

すべてのロックを取得する固定順序に従い、無条件のlock()の代わりにタイムアウト付きのtryLockを使用し、同時に保持するロックの数を最小限にしてください。ロックフリーデータ構造の使用もデッドロックのリスクを減らします。

LockにおけるConditionとは何ですか?

ConditionはLockに対するwait/notifyの類似物であり、複数の独立した待機キューを可能にします。各newCondition()呼び出しは個別のキューを作成し、synchronizedの単一キューと比較してスレッドの起床をより正確に制御できます。

モバイルアプリケーションにはどのLockを選ぶべきですか?

コルーチンを使用するAndroidの場合は、kotlinx.coroutinesのMutexを使用してください。スレッドをブロックする代わりにコルーチンを一時停止します。Swift 5.7+を使用するiOSの場合は、状態アクセスを自動的に同期するアクターが推奨されます。ReentrantLockはレガシーコードと低レベルシナリオに残してください。

まとめ

  • Lockはjava.util.concurrent.locksの明示的なロック管理インターフェースです。
  • ReentrantLockは再取得と公平性をサポートする主要な実装です。
  • ReadWriteLockは読み取り重視のシナリオのために読み取りと書き込みのロックを分離します。
  • StampedLockは最大のパフォーマンスのために楽観的読み取りを追加します。
  • タイムアウトとConditionsはsynchronizedに対するLockの主な利点です。
  • Deadlockは一貫したロック順序とtryLockの使用によって防止されます。
  • モバイル開発では、コルーチン(Android)とアクター(iOS)が推奨されます。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください