モバイル開発におけるDeadlock:その概要、原因、および相互ブロックを回避する方法

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

Deadlock(相互ブロック)とは、2つ以上のスレッドが他の参加者によって保持されているリソースの解放を無限に待機する状態です。Oracle Java Tutorials (2024)によると、Deadlockは各スレッドが別のスレッドに必要なロックを保持している循環待機で発生します。特別な検出ツールがない場合、Deadlockは可視エラーなしにアプリケーションの実行を完全に停止させます。

重要ポイント

  • Deadlock — 各スレッドが別のスレッドによって保持されているリソースを待機するスレッドの相互ブロック
  • Coffmanの4つの条件(Mutual Exclusion、Hold and Wait、No Preemption、Circular Wait)はDeadlockの発生に必要
  • DeadlockはStarvationと異なり、スレッドがブロックされておらず、循環依存でアクティブに待機している
  • Thread Dump — JVMおよびAndroid RuntimeでDeadlockを検出する主要ツール
  • ロック階層とリソース取得の統一順序 — 相互ブロックを防ぐ主要な方法

Deadlockとは?

Deadlockはマルチスレッドプログラミングにおいて、2つ以上のスレッドが互いに永久にブロックし合う状況です。各スレッドは別のスレッドに必要なリソースを保持し、不足しているリソースを取得するのを待つ間、それを解放しません。結果として、どのスレッドも実行を継続できません。

モバイル開発において、Deadlockは例外やクラッシュを引き起こさないため、特に重要です。アプリケーションはユーザーの操作に応答しなくなり(ANR — Application Not Responding)、唯一の解決策はプロセスを強制終了することです。Google(Android Performance Patterns、2023)によると、Google Play ConsoleのANRレポートの約15%はバックグラウンドスレッドでの相互ブロックに関連しています。

Deadlockと他の並行性問題の主な違いは、外部介入なしでは不可逆であることです。オペレーティングシステムのスケジューラはロックを強制的に取り消せないため、スレッドは自分からリソースを解放しません。これにより、スレッドはアクティブだが有用な作業をしていないLivelockとDeadlockが区別されます。

Deadlockの発生条件

1971年、Edward G. CoffmanはDeadlockの発生に必要な4つの必須条件を定式化しました。少なくとも1つが欠けている場合、相互ブロックは不可能です。これらの条件はCoffmanの条件として知られ、すべてのDeadlock防止アルゴリズムの基礎を形成しています。

相互排他(Mutual Exclusion)

リソースは一度に1つのスレッドのみが取得できます。リソースが複数のスレッドによる同時読み取りを許可する場合(例:読み取りモードのReadWriteLock)、Deadlockは発生しません。この条件はMutexとロックの性質そのものに由来します。

保持と待機(Hold and Wait)

スレッドは既に取得したリソースを保持し、同時に別のリソースの取得を待機します。スレッドが次のリソースを要求する前に現在のリソースを解放できる場合(二相ロッキングによる)、Hold and Wait条件は破られます。Androidでは、スレッドがデータベースロックを保持し、SharedPreferencesロックを取得しようとするときに現れることがよくあります。

非強制横取り(No Preemption)

オペレーティングシステムはスレッドからロックを強制的に奪うことはできません。リソースはスレッド自身が解放したときにのみ解放されます。一部のシステム(例:SQLite WALモード)では、個々の操作レベルで強制横取りが実装され、Deadlockのリスクを低減します。

循環待機(Circular Wait)

スレッドの閉じたチェーンが存在し、各スレッドがチェーン内の次のスレッドによって保持されているリソースを待機します。例えば、スレッドAがリソース1を保持しリソース2を待機し、スレッドBがリソース2を保持しリソース1を待機します。これは開発者がアーキテクチャ的に排除できる唯一の条件です — ロック階層を通じて。すべてのスレッドが厳密に定義されたグローバル順序でリソースを取得する場合、循環は物理的に不可能です。

実際には、Androidアプリケーションでは、異なるレベルのロックの暗黙的な交差によりDeadlockが最も頻繁に発生します:データベースロック(Room)、SharedPreferencesロック、インメモリコレクションロック。これらの各ロックは異なるコンポーネントによって管理され、取得順序の集中プロトコルがないと、開発者は意図せず循環を作り出します。

KotlinでのDeadlockコード例

相互ブロックの古典的な例を見てみましょう — 2つのスレッドが異なる順序でロックを取得する場合です。最初のスレッドがリソースAをロックしBを取得しようとし、2番目のスレッドがBをロックしAを取得しようとすると、Deadlockが発生します。

kotlin
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun operationA() {
        synchronized(lockA) {
            Thread.sleep(50)  // 作業シミュレーション
            synchronized(lockB) {
                println("operationA完了")
            }
        }
    }

    fun operationB() {
        synchronized(lockB) {  // 逆順序
            Thread.sleep(50)
            synchronized(lockA) {
                println("operationB完了")
            }
        }
    }
}

fun main() {
    val ex = DeadlockExample()
    Thread { ex.operationA() }.start()
    Thread { ex.operationB() }.start()
    // アプリケーションは永久にハングします — Deadlock!
}

この例では、operationAがlockAを取得し、operationBがlockBを取得します。次にそれぞれが2番目のロックを取得しようとし — 両方が無限に待機します。プログラムはエラーメッセージなしでハングします。修正する唯一の方法は、すべてのメソッドで同じロック取得順序を保証することです。

Deadlock vs Starvation vs Livelock

これら3つの並行性問題はよく混同されますが、そのメカニズムと結果は根本的に異なります。Deadlock — 完全停止、Starvation — リソースの無限待機、Livelock — アクティブなアイドル状態。違いを理解することは、正しい解決戦略を選択するために極めて重要です。

特性DeadlockStarvationLivelock
スレッドの状態ブロック済み(BLOCKED)準備完了(RUNNABLE)アクティブ(RUNNABLE)
作業の実行なしなしあり、ただし無駄
原因循環待機不公平なスケジューリング誤った競合処理
検出Thread Dump、タイムアウト進捗監視再試行カウンター

Starvation(枯渇)は、スケジューラが他のスレッドを優先して低優先度スレッドの実行を常に延期するときに発生します。Deadlockとは異なり、スレッドはブロックされていません — 実行準備はできていますがCPU時間を得られません。Androidでは、UIスレッドとServiceスレッドが常にアクティブな場合、低優先度のバックグラウンドスレッドが決して実行されないのが典型的なシナリオです。

Livelock(活性ロック)は、スレッドがブロックされていないものの、互いのアクションに無限に反応し有用な作業を行わない状況です。古典的な例え — 廊下で2人がすれ違おうとして、両方が同じ方向に避けようとします。Deadlockとは異なり、LivelockのスレッドはCPUを消費し、デバイスのバッテリーを消耗させます。

Deadlockの検出方法

Thread DumpはJVMおよびAndroid Runtimeで相互ブロックを検出する主要ツールです。ダンプ中、JVMはモニター間の依存関係グラフを自動的に分析し、Deadlockサイクルをマークします。Android Studioでは、Android ProfilerまたはADB Shellからのkill -3 PIDコマンドを通じてスレッドダンプを取得できます。

実行時の自動Deadlock検出はWatchdogタイマーを通じて実装されます。スレッドが指定されたタイムアウト内に操作を完了しない場合、watchdogがダンプを開始し、クラッシュレポーティングシステム(Firebase Crashlytics、Sentry)にレポートを送信します。Sentry(Issue Resolution Report、2024)によると、watchdogの設定によりDeadlock診断時間が数週間から数時間に短縮されます。

開発段階では、JetBrains ThreadSafe静的解析ツールおよびLock Checkerモジュールを備えたChecker Frameworkが効果的です。これらのツールはソースコードレベルでロック取得順序を分析し、潜在的な循環について警告します。さらに、Test-Driven Deadlock Detection — 数百のスレッドで異なるロック順序で操作を実行するストレステストが推奨されます。

特に注目すべきはCooperative Deadlock Detection — スレッドがグローバルレジストリを通じて取得したロックに関する情報を交換する方法です。スレッドが潜在的な循環を検出すると、すべてのリソースを解放して操作を再試行します。このアプローチは分散システム(Apache ZooKeeper、Google Chubby)で使用され、Jetpack Syncなどのライブラリを通じて徐々にモバイル開発に採用されています。

相互ブロックを防ぐ方法

ロック階層(Lock Ordering)

最も信頼できる方法は、アプリケーション全体でロック取得のグローバルな順序を確立することです。すべてのスレッドが常に小さい番号のロックを最初に取得し、次に大きい番号のロックを取得する場合、循環待機(Circular Wait条件)は不可能です。大規模プロジェクトでは、順序は文書化され、コードレビューを通じて検証されます。

タイムアウト付きTryLock

TryLockはスレッドを無期限にブロックせず、指定された時間内にロックが取得できない場合にfalseを返すロック方法です。JavaではReentrantLock.tryLock(timeout、TimeUnit)を介して、Kotlin Coroutinesではタイムアウト付きのMutex.withLockを介して実装されます。失敗した場合、スレッドは取得したすべてのリソースを解放し、後で再試行します。

銀行家のアルゴリズム(Banker's Algorithm)

銀行家のアルゴリズムはEdsger Dijkstraによって提案されたDeadlock防止の理論的方法です。リソース割り当てを銀行取引としてモデル化します:システムは、不安全な状態(deadlock)につながる可能性がある場合、リソースを割り当てません。実際には、スレッドの最大必要量を事前に知ることの難しさから、モバイル開発ではアルゴリズムが使用されることは稀ですが、その原理はSQLiteデータベースやファイルシステムで使用されています。

よくある質問

シングルスレッドアプリケーションでDeadlockは発生しますか?

いいえ、相互ブロックには少なくとも2つのスレッドが必要です。シングルスレッドコードでは、すべての操作が順次実行されるため、循環待機は不可能です。ただし、ファイルロックやプロセス間セマフォを使用する場合、プロセス間でDeadlockが発生する可能性があります。

Kotlin CoroutinesのDeadlockはスレッドのDeadlockとどう違いますか?

コルーチンでは、Deadlockはサスペンド関数のレベルで発生し、OSスレッドをブロックしないため、気づきにくくなっています。kotlinx.coroutinesのMutexはサスペンディングロックであり、スレッドをブロックしませんが、コルーチンは実行されません。検出には、kotlinx-coroutines-debugモジュールのDebugProbesを使用してください。

AndroidのSQLiteにおけるDeadlockとは?

SQLite Deadlockは、2つのデータベース接続が異なる順序でトランザクションを実行しようとするときに発生します。SQLiteはそのような状況を検出し、SQLITE_BUSYまたはSQLITE_LOCKEDエラーコードを返します。Androidでは、単一のデータベースインスタンスと@Transactionによるトランザクションを持つRoomを使用することが推奨され、接続間のDeadlockを排除します。

AndroidはどのようにDeadlockを検出しますか?

Android Runtimeには、ANR(Application Not Responding)が発生したときに実行される組み込みのDeadlock検出器があります。システムはアプリケーションの全スレッドのThread Dumpを分析し、相互ブロックをマークします。結果は/data/anr/traces.txtおよびGoogle Play ConsoleのANR Reportsセクションで利用できます。

本番環境でDeadlockが見つかった場合の対処法は?

まず、アプリケーションのすべてのスレッドのThread Dumpを取得します。各スレッドがどのロックを保持し、どのロックを取得しようとしているかを分析します。制限時間を超えた場合に自動ダンプするWatchdogタイマーを実装します。修正後、再発を防ぐためにCIパイプラインにThreadSafety lintルールを追加します。

まとめ

  • Deadlock — スレッドが互いに保持しているリソースを無限に待機する相互ブロック
  • Coffmanの4つの条件(Mutual Exclusion、Hold and Wait、No Preemption、Circular Wait)はDeadlockに必要
  • Thread Dump — JVMおよびAndroid Runtimeで相互ブロックを検出する標準的な方法
  • 統一されたグローバル順序を持つロック階層が循環待機条件を完全に排除
  • タイムアウト付きTryLockが無限待機を防ぎ、リソースが利用できない場合の適切な処理を可能にする
  • Deadlock vs Starvation — Deadlockではスレッドがブロックされ、Starvationでは実行準備ができているがCPUを得られない
  • Watchdogタイマーと静的解析ツール(ThreadSafe、Checker Framework) — CI/CDにおけるDeadlockの基本的な保護

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

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

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

こちらもお読みください