Livelock(アクティブロック)とは、マルチスレッドプログラミングにおいて、スレッドがブロックされていないものの、互いの動作に無限に反応し続け、有用な処理を行わない状況です。Baeldung(Java Concurrency Guide、2024)によると、Livelockではスレッドが隣接スレッドの状態に応じて常に状態を変更しますが、どのスレッドも目標を達成できません。Deadlockとは異なり、LivelockはCPUを100%消費するため、モバイルデバイスのバッテリーを急速に消耗させます。
重要ポイント
Livelock(アクティブロック)とは、マルチスレッドシステムにおいて、スレッドがブロックされてはいないものの、有用な処理を行わない状況です。各スレッドは続行できないことを検出し、それを修正しようとしますが、その動作が他のスレッドで同じ反応を引き起こします。その結果、システムは進行することなく、無限に状態間を切り替えます。
Livelockの古典的な例えは、狭い廊下で二人の人が出会う状況です。それぞれが相手に道を譲ろうと横にそれますが、両者が同時に同じ動きをして、再び向かい合ってしまいます。彼らは立ち止まってはいません(それはDeadlockです)が、活発に動いているにもかかわらず、決してすれ違えません。プログラミングでは、これはスレッドがリソースを絶えず解放と再取得を繰り返すことに相当します。
モバイル開発において、Livelockは特に危険です。ユーザーには気づかれず、アプリはフリーズせず、インターフェースもブロックされませんが、バックグラウンドスレッドによる100%のCPU負荷のため、バッテリーが2〜3倍速く消耗します。Googleのテスト(Android Battery Optimization、2023)によると、バックグラウンドServiceでのLivelockはデバイスのバッテリー寿命を40%も短縮する可能性があります。
Livelockは、複数のスレッドが同じ競合反応戦略を使用する場合に発生します。スレッドAがリソースを取得できずに現在のリソースを解放し、スレッドBが同時に同じことを行うと、両方がサイクルを繰り返し、状況が無限に続きます。これは特に、TryLockと失敗時の自動解放を使用するアルゴリズムに特徴的です。
スレッドが再試行前に固定遅延を使用すると、同期サイクルに入る可能性があります。両方のスレッドが同じ時間待機すると、再び同時にリソースの取得を試み、同時に解放します。この問題は、EthernetのCSMA/CDアルゴリズムのように、ランダム要素(ジッター)を伴う指数バックオフを使用することで解決されます。
モバイル開発では、タスクキューの不適切な実装が原因でLivelockが頻繁に発生します。例えば、ワーカースレッドがメッセージの処理を完了したものの、優先順位付けのロジックによって、同じことをする別のワーカーに常に制御を委譲してしまう場合です。このような状況は、標準的でないRejectedExecutionHandlerポリシーを持つカスタムThreadPoolExecutorに典型的です。
2つのスレッドがTryLockを使用し、失敗時にリソースを解放する状況を考えます。アクティブロックは、両方のスレッドが同じロジックを適用し、同期的に再試行するために発生します。
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit
class LivelockWorker(private val name: String,
private val lock1: ReentrantLock,
private val lock2: ReentrantLock) {
fun execute() {
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
println("$name — 完了!")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // 解放して再試行
}
}
Thread.sleep(50) // 同じ遅延 — Livelockの主要因子
}
}
}
LivelockWorkerの2つのインスタンスがlock1とlock2の取得順序を変えて実行されると、アクティブロックに入ります。それぞれが最初のリソースを取得し、2番目を取得できず、最初を解放し、50ms待機して再試行することを無限に繰り返し、CPUを消費します。修正方法は、遅延にランダム要素(ジッター)を追加し、再試行回数を制限することです。
修正版ではランダムジッター付き指数バックオフを使用します。失敗するたびに、ランダム乗数で待機時間が増加し、スレッド間の同期が解除されます。
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("成功!")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("5回の試行後に失敗")
}
外見上の類似性にもかかわらず、LivelockとDeadlockは根本的に異なるメカニズムと結果をもたらします。DeadlockではスレッドがブロックされCPUを消費しません — アプリケーションは単にフリーズします。LivelockではスレッドはアクティブでCPUを100%消費しますが、有用な処理は行いません。解決戦略の選択は、ロックの種類を正しく識別することに依存します。
| パラメータ | Deadlock | Livelock |
|---|---|---|
| スレッド状態 | BLOCKED / WAITING | RUNNABLE |
| CPU消費 | 最小 | 高(90-100%) |
| バッテリー消費 | 低 | 高 |
| 検出 | Thread Dump | CPU Profiler + 視覚分析 |
| 典型的な原因 | ロック取得順序の不一致 | 同じ競合反応戦略 |
| 修正 | ロック階層化 | リトライ制限 + 指数バックオフ |
モバイル開発において、実際的な違いは計り知れません。DeadlockはANRとアプリの再起動を引き起こし、Google Play Consoleを通じて検出・報告されます。Livelockは気づかれず、アプリは動作しているように見えますが、バッテリーは1時間で切れ、ユーザーは単にアプリを削除します。Firebase Analytics(App Retention Report、2024)によると、バックグラウンドで過剰にバッテリーを消費するアプリは68%のユーザーに削除されます。
Livelockの検出はDeadlockよりも困難です。システムは明白な信号を発しないためです — 例外もANRもエラーメッセージもありません。主な診断方法はAndroid StudioのCPU Profilerです。スレッドが常にRUNNABLE状態にあるにもかかわらず、有用なI/Oや計算処理を行っていない場合、Livelockが疑われます。
追加の指標は、アプリがアイドル状態のときの異常なバッテリー消費です。Android Battery Historian(Android SDKのツール)は、コンポーネントごとのエネルギー消費グラフを作成します。CPU Wakelockが明らかな理由なく保持されている場合は、Method Tracingを実行し、疑わしいスレッドのコールスタックを分析します。
コードレベルでは、threadIdとタイムスタンプ付きの再試行のログ記録が役立ちます。ログが1秒間に何千もの再試行を成功なしに示している場合、それはLivelockです。Hystrixのようなサーキットブレーカーや、しきい値を超えた場合に操作を無効化してCrashlyticsを通じて開発者に通知するリトライカウンターの実装が推奨されます。
最も簡単で信頼性の高い方法は、リソース取得の試行回数を制限することです。N回の試行後に操作が失敗した場合、スレッドはエラー状態に移行し、ユーザーに通知します。Nは経験的に選択されます。モバイルアプリの場合、通常3〜5回です。これにより、高負荷時のまれな誤検出を代償に、無限Livelockを完全に排除できます。
試行間の固定遅延の代わりに、ランダム要素を伴う指数関数的に増加する休止を使用します。式:delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter)。このアプローチはスレッドの同期を断ち切るだけでなく、高競合下でのシステム全体の負荷も軽減します。ネットワークプロトコルアルゴリズムで使用され、Firebase Realtime DatabaseのリトライロジックとしてGoogleが推奨しています。
異なるスレッドに異なる戦略を割り当てることで、Livelockの根本原因 — 競合に対する同一の反応 — を排除します。例えば、高優先度スレッドは解放せずにリソースを取得し、低優先度スレッドは解放して待機します。モバイル開発では、UIスレッドがロック取得時に優先権を持ち、バックグラウンドワーカースレッドはタイムアウト付きのTryLockを使用できます。
一部のアーキテクチャでは、設計レベルでLivelockを防止します。リソースの解放を一方向のみに制限します。例えば、スレッドAが固定チャネルを通じて常にスレッドBに制御を委譲し、Bが決してAに制御を戻そうとしない場合、反応サイクルは不可能です。単方向処理段階を持つパイプラインアーキテクチャは、Android CameraXやMediaPipeの隣接段階間のLivelockを完全に排除します。
よくある質問
無限ループは外部要因に依存せず、他のスレッドと相互作用せずに単一の操作を繰り返します。Livelockは常に他のスレッドの動作への反応です。スレッドが隣接スレッドの状態に応じて動作を変更し、閉じたフィードバックループを形成します。Livelockの場合のThread Dumpは、一定のコンテキストスイッチングを示します。
データベースでは、他のトランザクションによるロックのためトランザクションが常に延期される場合にLivelockが発生します。例えば、DBMSがwait-dieアルゴリズムを使用する場合、開始時間が早いトランザクションが新しいトランザクションと競合すると、ロールバックして再起動しますが、毎回同じ競合に遭遇します。これはランダム化された再起動遅延で解決されます。
一部のシステムでは、LivelockはDeadlockよりも好ましい場合があります。スレッドがアクティブなままで問題を検出できるためです。例えば、楽観的ロック(optimistic locking)アルゴリズムでは、リトライ制限が最終的な完了を保証する限り、livelock的な動作は許容されます。これはパフォーマンスと進行保証のトレードオフです。
Livelockはテストでの再現が非常に困難です。スレッドの正確なタイミングの一致が必要だからです。単体テストは決定論的に実行され、アクティブロックを明らかにすることはほとんどありません。負荷下での繰り返し実行とプロファイラでのCPU消費監視を伴うストレステストの使用が推奨されます。
サーバーでは、Livelockはパフォーマンス低下とタイムアウトを引き起こしますが、サーバーは水平方向にスケーリングできます。Androidでは、Livelockはバッテリーを消耗させ、デバイスを過熱させ、最悪のユーザー体験を生み出します。さらに、モバイルデバイスはCPUコア数が限られているため、Livelockはより迅速にシステム全体の動作不能状態を引き起こします。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。