モバイルアプリケーションにおけるMutex — その概要、動作原理、および相互排他の適用

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

Mutex(相互排他)は、任意の時点で1つのスレッドのみがコードのクリティカルセクションを実行できることを保証する同期プリミティブです。Microsoft Docs (Synchronization Objects, 2024)によると、Mutexの基本原則は所有権です:Mutexを取得したスレッドがその所有者となり、クリティカルセクションを抜けるときにのみ解放します。Mutexは、レースコンディション(Race Condition)を防止し、マルチスレッドアプリケーションでデータの整合性を確保するための基本的なツールです。

重要なポイント

  • Mutexは、一度に1つのスレッドのみがリソースにアクセスできることを保証する相互排他機構です
  • 所有権(ownership)はMutexの重要な特徴です:ロックを取得したスレッドのみが解放できます
  • セマフォとは異なりカウンタ≥2の場合、Mutexの状態は0または1のみです(バイナリセマフォ)
  • Mutexのデッドロックは、複数のミューテックスを誤った順序で取得したときに発生します
  • Kotlin Coroutinesのsuspending MutexはOSスレッドをブロックせず、古典的なReentrantLockとは異なります

Mutexとは?

Mutex(Mutual Exclusion — 相互排他の略)は、マルチスレッド環境で共有リソースへのアクセスを管理する同期オブジェクトです。スレッドがクリティカルセクションに入ると、Mutexを取得します。別のスレッドが同じMutexを取得しようとすると、最初のスレッドがロックを解放するまで待機状態になります。

Mutexのアーキテクチャは、1965年にEdsger Dijkstraによって設計されたTHEオペレーティングシステムにまで遡ります。Dijkstraはセマフォの概念を導入し、そこからMutexが特別なケースとして後に登場しました — 所有権サポート付きのバイナリセマフォです。現代のOS(Linux、Windows、Android)はカーネルレベルでMutexを実装しており、異なるプロセス間でも正しい同期を保証します。

Mutexの重要な特性は所有権(ownership)です。ミューテックスを取得したスレッドのみが解放できます。これによりMutexはバイナリセマフォと区別されます。バイナリセマフォでは任意のスレッドがシグナル(V操作)を実行できます。所有権により、別のスレッドによる誤ったロック解放が防止され、モバイル開発における一般的な同期シナリオでMutexがより安全になります。Android Developer Docs(Processes and Threads、2024)によると、高競合下でsynchronizedの代わりにMutexを使用すると、パフォーマンスが30%向上する可能性があります。

Mutexの仕組み

状態と操作

Mutexは2つの状態のいずれかです:ロック済み(locked) — スレッドによって取得済み;またはアンロック(unlocked) — 未取得。2つの基本操作があります:lock()(取得)とunlock()(解放)。Mutexがすでにロックされている場合、lock()を呼び出したスレッドはロックが解放されるまでブロックされます。JVMでは、ブロックされたスレッドはBLOCKED状態に遷移し、CPUを消費しません。

待機スレッドのスケジューリング

Mutexが解放されると、システムはどの待機スレッドがロックを取得するかを選択します。非フェア(non-fair)スケジューリングでは、選択はミューテックスを解放したばかりのスレッドに下される可能性があります — これによりスループットは向上しますが、スターベーション(枯渇)が発生する可能性があります。フェアスケジューラはFIFOキューを使用します:最初の待機スレッドが最初にロックを取得します。ReentrantLock(true)はまさにこのメカニズムを実装しています。

再帰的取得(リエントランシー)

Java/KotlinのほとんどのMutex実装はリエントラント取得をサポートしています。スレッドがすでにMutexを所有していて再度lock()を呼び出すと、操作は成功します — Mutexは自分自身をブロックしません。再帰カウンタが増加し、スレッドはlock()と同じ回数だけunlock()を呼び出す必要があります。これは再帰呼び出しやネストされたクリティカルセクションにとって重要です。

KotlinでのMutex使用例

典型的なタスクを考えてみましょう — ReentrantLock(Java/Kotlinの古典的なMutex)を使用して共有カウンタをレースコンディションから保護します。Mutexがないと、コードは誤った結果を生成します。Mutexがあれば、1000個すべてのスレッドが確実にカウンタ値を増加させます。

kotlin
import java.util.concurrent.locks.ReentrantLock

class MutexCounter {
    private val mutex = ReentrantLock()
    private var count = 0

    fun increment() {
        mutex.lock()
        try {
            count++  // クリティカルセクション
        } finally {
            mutex.unlock()  // 必須のfinally
        }
    }

    fun getCount(): Int {
        mutex.lock()
        try {
            return count
        } finally {
            mutex.unlock()
        }
    }
}

fun main() = runBlocking {
    val counter = MutexCounter()
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            counter.increment()
        }
    }
    jobs.forEach { it.join() }
    println(counter.getCount())  // 常に1000
}

finallyブロックに注意してください — Mutexを扱う際の必須パターンです。クリティカルセクション内で例外が発生すると、unlock()が呼び出されず、Mutexは永久にロックされたままになります — これによりデッドロックが発生します。finallyブロックは、セクションの実行がどのように終了するかに関係なく、Mutexの解放を保証します。

Kotlinでの代替アプローチは、withLock拡張関数を使用することです。これはfinallyを使用してlock/unlockを自動的に処理します。

kotlin
fun increment() {
    mutex.withLock {  // lock + try/finally 自動的
        count++
    }
}

fun getCount(): Int = mutex.withLock { count }

Mutex vs セマフォ vs モニター

これら3つの同期メカニズムはしばしば混同されますが、異なる特性と使用ケースがあります。Mutexは所有権を持つバイナリです。セマフォは所有権のない許可カウンタです。モニターはMutexと条件変数を組み合わせた高レベルメカニズムです。これらの違いを理解することは、特定のタスクに適したツールを選択するために極めて重要です。

パラメータMutexセマフォモニター
タイプバイナリ (0/1)カウント (0..N)バイナリ + 条件
所有権所有者のみunlock可能任意のスレッドがsignal可能所有者のみ
リエントランシー通常はい (reentrant)いいえはい
条件付き待機いいえ(Conditionが必要)いいえ組み込み (wait/notify)
Java/Kotlinの例ReentrantLockSemaphore(permits)synchronized

Mutexを選ぶべき時:単一のリソースを同時アクセスから保護する必要がある場合 — 例えば、共有コレクション、ファイル、またはカウンタ。セマフォを選ぶべき時 — リソースプールへの同時アクセス数を制限する必要がある場合、例えば5接続のデータベース接続プール。モニターを選ぶべき時 — 条件付き待機による同期が必要な場合、例えばwait/notifyを使用したプロデューサー-コンシューマーキュー。現代のAndroid開発では、synchronizedはしばしばReentrantLockまたはkotlinx.coroutines Mutexに置き換えられます。

Mutex使用時の一般的なエラー

finallyでのunlock忘れ

最も一般的なエラーは、unlock()を呼び出すためのfinallyブロックの欠落です。クリティカルセクションで例外が発生すると、Mutexはロックされたままになり、他のスレッドは永久に待機します。例外が絶対に起こらないと確信している場合でも — 常にtry/finallyまたはwithLockを使用してください。これは防御的プログラミングの原則であり、メモリ不足やConfiguration Changesによって例外が発生する可能性があるモバイル開発では特に重要です。

Mutex取得順序の不一致

アプリケーションが複数のMutexを使用する場合、一貫した取得順序を確立することが極めて重要です。スレッドAがM1 → M2を取得し、スレッドBがM2 → M1を取得すると、デッドロックが発生します。大規模プロジェクト(5万行以上のコード)では、ロック順序はアーキテクチャ決定として文書化され、リンターによって検証されます。IntelliJ IDEAのLock Checkerツールは、不整合なロック取得順序を自動的に検出します。

クリティカルセクションが長すぎる

Mutexを1〜2ミリ秒以上保持することは、設計不良の兆候です。クリティカルセクションには最小限の必要な操作のみを含めるべきです。ネットワークリクエスト、ファイルI/O、複雑な計算はロックされたブロックの外部で実行する必要があります。Androidでは、UIスレッドでの長時間のロック保持はフレームドロップ(ジャンク)やANRを引き起こします。クリティカルセクションが主に読み取り操作で構成されている場合は、ReadWriteLockを使用してください。

Kotlin CoroutinesにおけるMutex

kotlinx.coroutinesライブラリは独自のMutex実装を提供し、これは古典的なReentrantLockとは根本的に異なります。主な違いは、suspending MutexがOSスレッドをブロックせず、ロックが解放されるまでコルーチンを一時停止することです。つまり、現在のコルーチンがMutexを待機している間、スレッドは他のコルーチンを実行できます。

kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class CoroutineCounter {
    private val mutex = Mutex()
    private var count = 0

    suspend fun increment() {
        mutex.withLock {  // suspending — スレッドをブロックしない
            count++
        }
    }

    suspend fun getCount(): Int = mutex.withLock { count }
}

kotlinx Mutexの主な特徴:非リエントラント(non-reentrant) — ReentrantLockとは異なり、コルーチンはすでに所有しているMutexを再取得できません。これが必要な場合は、Mutexの代わりにSemaphore(1)を使用してください。さらに、kotlinx.coroutinesのMutexはノンブロッキングです:suspendによる一時停止を使用するため、プールスレッドをブロックしません。

実際には、コルーチンコードでは2つの理由でsuspending Mutexが古典的なReentrantLockより好まれます:スケーラビリティ — 1つのコルーチンがMutexを待機している間にスレッドが他のコルーチンを処理し、システムスループットが向上します;BlockedThreadがない — ブロックされたスレッドのスタックを保存するリソースが消費されません。JetBrains(Kotlin Coroutines Guide、2024)によると、100以上のコルーチンでsuspending Mutexを使用するとスループットが40%向上します。

よくある質問

Mutexとバイナリセマフォの違いは何ですか?

所有権(ownership)が根本的な違いです。Mutexはどのスレッドが取得したかを記憶し、そのスレッドのみが解放できます。バイナリセマフォ(Semaphore(1))には所有者がいません — 任意のスレッドがrelease()を呼び出せます。したがって、Mutexの方が安全です:別のスレッドが誤って他人のロックを解放することはできませんが、セマフォでは可能です。

Mutexとsynchronizedはいつ使い分けるべきですか?

synchronizedはよりシンプルで短いです — タイムアウトや公平性制御のない単純なクリティカルセクションに使用してください。ReentrantLockは、タイムアウト付きTryLock、フェアスケジューリング、Condition Variables、または待機スレッドの割り込み(lockInterruptibly)が必要な場合に使用してください。コルーチンでは、常にkotlinx.coroutines.sync.Mutexを使用してください。

スピンロックとは何ですか?Mutexとの違いは?

スピンロックは、スレッドがスリープせずにループ内でロック状態をチェックしながら回転(spin)するロックです。スピンロックはCPUを消費しますがコンテキストスイッチを行わないため、短いクリティカルセクション(最大10命令)に有利です。MutexはスレッドをBLOCKED状態にし、コンテキストスイッチのために10〜50マイクロ秒多くコストがかかりますが、CPUを無駄に消費しません。

MutexはOSレベルでどのように実装されていますか?

Linuxカーネルレベルでは、Mutexはfutex(fast userspace mutex)を介して実装されています。スレッドはまず、アトミックCAS(Compare-And-Swap)命令を使用してユーザースペースでロックを取得しようとします。Mutexが空いている場合 — syscallなしで取得が行われます。ビジーの場合 — スレッドはsyscall futex(FUTEX_WAIT)を行いスリープします。解放時に、syscall futex(FUTEX_WAKE)が1つの待機スレッドを起こします。

Mutexはプロセス間で使用できますか?

はい、プロセス間Mutex(inter-process mutex)が存在します。WindowsではNamed Mutex、LinuxではPTHREAD_PROCESS_SHARED属性を持つpthread_mutexattr_setpsharedです。AndroidのBionic libcもファイル記述子を介してプロセス間Mutexをサポートしています。プロセス間Mutexは、異なるアプリケーション間、またはプロセスとその子プロセス間の同期に使用されます。

まとめ

  • Mutexは、一度に1つのスレッドのみがクリティカルセクションを実行することを保証する相互排他プリミティブです
  • 所有権(ownership)がMutexをバイナリセマフォと区別します — 所有者スレッドのみが解放できます
  • ReentrantLockはJava/Kotlinにおける古典的なMutex実装で、リエントラント取得とTryLockをサポートします
  • 例外によるデッドロックを防ぐためにfinallyブロックまたはwithLockが必須です
  • kotlinx.coroutinesのsuspending MutexはOSスレッドをブロックせず、コルーチンを一時停止します
  • 複数のMutexの一貫した取得順序が複雑なシステムでデッドロックを回避する唯一の方法です
  • スターベーションなしのマルチスレッドアプリケーションパフォーマンスには短いクリティカルセクション(最大1〜2 ms)が鍵です

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

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

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

こちらもお読みください