Mutex (взаимное исключение) — это примитив синхронизации, который гарантирует, что только один поток может выполнять критическую секцию кода в каждый момент времени. Согласно Microsoft Docs (Synchronization Objects, 2024), основной принцип Mutex — владение: поток, захвативший Mutex, становится его владельцем и освобождает его только при выходе из критической секции. Mutex — фундаментальный инструмент для предотвращения Race Condition и обеспечения целостности данных в многопоточных приложениях.
Главное
Mutex (сокращение от Mutual Exclusion — взаимное исключение) — это объект синхронизации, который управляет доступом к общему ресурсу в многопоточной среде. Когда поток входит в критическую секцию, он захватывает Mutex. Если другой поток пытается захватить тот же Mutex, он переводится в состояние ожидания до освобождения блокировки первым потоком.
Архитектура Mutex восходит к операционной системе THE, разработанной Edsger Dijkstra в 1965 году. Именно Dijkstra ввёл концепцию семафоров, из которых позже выделились Mutex как частный случай — бинарный семафор с поддержкой владения. Современные ОС (Linux, Windows, Android) реализуют Mutex на уровне ядра, что обеспечивает корректную работу при синхронизации даже между разными процессами.
Ключевое свойство Mutex — ownership (владение). Только поток, захвативший мьютекс, может его освободить. Это отличает Mutex от бинарного семафора, где любой поток может выполнить сигнал (V-операцию). Владение предотвращает случайное освобождение блокировки другим потоком, что делает Mutex более безопасным для типичных сценариев синхронизации в мобильной разработке. По данным Android Developer Docs (Processes and Threads, 2024), использование Mutex вместо synchronized может повысить производительность на 30% при высокой конкуренции.
Mutex находится в одном из двух состояний: заблокирован (locked) — захвачен потоком; свободен (unlocked) — не захвачен. Две базовые операции — lock() (захват) и unlock() (освобождение). Если Mutex уже захвачен, поток, вызывающий lock(), блокируется до момента освобождения. В JVM блокированный поток переходит в состояние BLOCKED и не потребляет CPU.
Когда Mutex освобождается, система выбирает, какой из ожидающих потоков получит блокировку. При несправедливом (non-fair) планировании выбор может пасть на поток, который только что освободил мьютекс, — это повышает пропускную способность, но может привести к Starvation (голоданию). Справедливый (fair) планировщик использует очередь FIFO: первый ожидающий поток получает блокировку первой. ReentrantLock(true) реализует именно такой механизм.
Большинство реализаций Mutex в Java/Kotlin поддерживают рекурсивный (reentrant) захват. Если поток уже владеет Mutex и снова вызывает lock(), операция успешна — Mutex не блокирует сам себя. При этом счётчик рекурсии увеличивается, и поток должен вызвать unlock() столько же раз, сколько lock(). Это важно для рекурсивных вызовов и вложенных критических секций.
Рассмотрим типичную задачу — защита общего счётчика от Race Condition с помощью ReentrantLock (классический Mutex в Java/Kotlin). Без Mutex код давал бы неверный результат; с Mutex все 1000 потоков гарантированно увеличивают значение счётчика.
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 останется заблокированным навсегда — это приведёт к Deadlock. Блок finally гарантирует освобождение Mutex при любом исходе выполнения секции.
Альтернативный подход в Kotlin — использование extension-функции withLock, которая автоматически обрабатывает lock/unlock с finally.
fun increment() {
mutex.withLock { // lock + try/finally автоматически
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Эти три механизма синхронизации часто путают, хотя они имеют разные свойства и области применения. Mutex — бинарный, с владением. Semaphore — счётчик разрешений, без владения. Monitor — высокоуровневый механизм, объединяющий Mutex с условными переменными (condition variables). Понимание различий критически важно для выбора правильного инструмента под конкретную задачу.
| Параметр | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Тип | Бинарный (0/1) | Счётный (0..N) | Бинарный + условия |
| Владение | Только владелец может unlock | Любой поток может signal | Только владелец |
| Рекурсивность | Обычно да (reentrant) | Нет | Да |
| Условное ожидание | Нет (нужен Condition) | Нет | Встроено (wait/notify) |
| Пример в Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Когда выбирать Mutex: нужно защитить один ресурс от одновременного доступа — например, разделяемую коллекцию, файл, или счётчик. Когда выбирать Semaphore — нужно ограничить количество одновременных доступов к пулу ресурсов, например пул соединений с БД на 5 подключений. Когда выбирать Monitor — нужна синхронизация с условным ожиданием, например producer-consumer очередь через wait/notify. В современной Android-разработке synchronized часто заменяют на ReentrantLock или kotlinx.coroutines Mutex.
Самая распространённая ошибка — отсутствие finally-блока для вызова unlock(). Если в критической секции возникает исключение, Mutex остаётся заблокированным, и другие потоки ждут вечно. Даже если вы уверены, что исключения невозможны — всегда используйте try/finally или withLock. Это принцип defensive programming, особенно важный в мобильной разработке, где исключения могут возникать из-за нехватки памяти или Configuration Changes.
Когда в приложении используется несколько Mutex, критически важно устанавливать единый порядок их захвата. Если Thread A захватывает M1 → M2, а Thread B захватывает M2 → M1, возникает Deadlock. В крупных проектах (свыше 50 тысяч строк кода) порядок блокировок документируется в архитектурном решении и проверяется линтерами. Инструмент Lock Checker в IntelliJ IDEA автоматически выявляет непоследовательный порядок захвата блокировок.
Удержание Mutex дольше 1-2 миллисекунд — признак неправильного дизайна. Критическая секция должна содержать только минимально необходимые операции. Сетевые запросы, файловый ввод-вывод и сложные вычисления должны выполняться за пределами заблокированного блока. В Android длительное удержание блокировки в UI-потоке приводит к пропуску кадров (jank) и ANR. Используйте ReadWriteLock, если критическая секция в основном состоит из операций чтения.
Библиотека kotlinx.coroutines предоставляет собственную реализацию Mutex, которая фундаментально отличается от классического ReentrantLock. Главное отличие — suspending Mutex не блокирует поток ОС, а приостанавливает корутину до момента освобождения блокировки. Это означает, что поток может выполнять другие корутины, пока текущая ожидает Mutex.
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, которым уже владеет. Если это необходимо, используйте Semaphore(1) вместо Mutex. Кроме того, Mutex из kotlinx.coroutines — не-blocking: он использует приостановку через suspend, что позволяет не блокировать поток пула.
На практике suspending Mutex предпочтительнее классического ReentrantLock в корутинном коде по двум причинам: масштабирование — одна корутина ожидает Mutex, а поток обслуживает другие корутины, что увеличивает пропускную способность системы; отсутствие BlockedThread — не тратится ресурс на хранение стека заблокированного потока. По данным JetBrains (Kotlin Coroutines Guide, 2024), использование suspending Mutex повышает пропускную способность на 40% при 100+ корутинах.
Часто задаваемые вопросы
Владение (ownership) — принципиальное отличие. Mutex запоминает, какой поток его захватил, и только этот поток может его освободить. Бинарный семафор (Semaphore(1)) не имеет владельца — любой поток может выполнить release(). Поэтому Mutex безопаснее: другой поток не может случайно освободить чужую блокировку, а семафор может.
synchronized проще и короче — используйте его для простых критических секций без таймаутов и без контроля справедливости. ReentrantLock используйте, когда нужен TryLock с таймаутом, fair-планирование, Condition Variables или прерывание ожидающего потока (lockInterruptibly). Для корутин всегда используйте kotlinx.coroutines.sync.Mutex.
Spinlock — это блокировка, при которой поток не засыпает, а в цикле (spin) проверяет состояние блокировки. Spinlock потребляет CPU, но не переключает контекст, что делает его выгодным для коротких критических секций (до 10 инструкций). Mutex переводит поток в состояние BLOCKED, что дороже на 10-50 микросекунд из-за переключения контекста, но не тратит CPU.
На уровне ядра Linux Mutex реализован через futex (fast userspace mutex). Поток сначала пытается захватить блокировку в userspace через атомарную инструкцию CAS (Compare-And-Swap). Если Mutex свободен — захват происходит без syscall. Если занят — поток делает syscall futex(FUTEX_WAIT) и засыпает. При освобождении syscall futex(FUTEX_WAKE) пробуждает один ожидающий поток.
Да, существуют межпроцессные Mutex (inter-process mutex). В Windows это Named Mutex, в Linux — pthread_mutexattr_setpshared с атрибутом PTHREAD_PROCESS_SHARED. В Android Bionic libc также поддерживает межпроцессные Mutex через файловые дескрипторы. Межпроцессные Mutex используются для синхронизации между разными приложениями или между процессом и его дочерними процессами.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также