Mutex (exclusion mutuelle) est une primitive de synchronisation qui garantit qu'un seul thread peut exécuter une section critique de code à un moment donné. Selon Microsoft Docs (Synchronization Objects, 2024), le principe fondamental de Mutex est la propriété : un thread qui acquiert un Mutex en devient propriétaire et ne le libère qu'en sortant de la section critique. Mutex est un outil fondamental pour prévenir les conditions de course (Race Condition) et garantir l'intégrité des données dans les applications multithread.
Points Clés
Mutex(abréviation de Mutual Exclusion — exclusion mutuelle) est un objet de synchronisation qui gère l'accès à une ressource partagée dans un environnement multithread. Lorsqu'un thread entre dans une section critique, il acquiert le Mutex. Si un autre thread tente d'acquérir le même Mutex, il est placé en état d'attente jusqu'à ce que le verrou soit libéré par le premier thread.
L'architecture de Mutex remonte au système d'exploitation THE, conçu par Edsger Dijkstra en 1965. Dijkstra a introduit le concept de sémaphores, dont Mutex a ensuite émergé comme un cas particulier — un sémaphore binaire avec support de propriété. Les OS modernes (Linux, Windows, Android) implémentent Mutex au niveau du noyau, garantissant une synchronisation correcte même entre différents processus.
La propriété clé de Mutex est la propriété (ownership). Seul le thread qui a acquis le mutex peut le libérer. Cela distingue Mutex d'un sémaphore binaire, où n'importe quel thread peut effectuer un signal (opération V). La propriété empêche la libération accidentelle du verrou par un autre thread, rendant Mutex plus sûr pour les scénarios de synchronisation typiques dans le développement mobile. Selon Android Developer Docs (Processes and Threads, 2024), l'utilisation de Mutex au lieu de synchronized peut améliorer les performances de 30% en cas de forte contention.
Un Mutex se trouve dans l'un de deux états : verrouillé (locked) — acquis par un thread ; ou libre (unlocked) — non acquis. Deux opérations de base existent : lock() (acquérir) et unlock() (libérer). Si le Mutex est déjà verrouillé, le thread appelant lock() est bloqué jusqu'à la libération du verrou. Dans la JVM, un thread bloqué passe à l'état BLOCKED et ne consomme pas de CPU.
Lorsqu'un Mutex est libéré, le système sélectionne quel thread en attente reçoit le verrou. Avec l'ordonnancement non équitable (non-fair), le choix peut tomber sur le thread qui vient de libérer le mutex — cela augmente le débit mais peut conduire à la famine (Starvation). Un ordonnanceur équitable (fair) utilise une file FIFO : le premier thread en attente reçoit le verrou en premier. ReentrantLock(true) implémente exactement ce mécanisme.
La plupart des implémentations de Mutex en Java/Kotlin supportent l'acquisition réentrante. Si un thread possède déjà le Mutex et appelle à nouveau lock(), l'opération réussit — le Mutex ne se bloque pas lui-même. Le compteur de récursion augmente, et le thread doit appeler unlock() autant de fois que lock(). Ceci est important pour les appels récursifs et les sections critiques imbriquées.
Considérons une tâche typique — protéger un compteur partagé des conditions de course en utilisant ReentrantLock (Mutex classique en Java/Kotlin). Sans Mutex, le code donnerait des résultats incorrects ; avec Mutex, les 1000 threads incrémentent de manière fiable la valeur du compteur.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // section critique
} finally {
mutex.unlock() // finally obligatoire
}
}
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()) // Toujours 1000
}
Faites attention au bloc finally — un modèle obligatoire lors du travail avec Mutex. Si une exception se produit dans la section critique, unlock() ne sera pas appelé et le Mutex restera verrouillé pour toujours — cela mène à un Deadlock. Le bloc finally garantit la libération du Mutex indépendamment de la façon dont l'exécution de la section se termine.
Une approche alternative en Kotlin consiste à utiliser la fonction d'extension withLock, qui gère automatiquement lock/unlock avec finally.
fun increment() {
mutex.withLock { // lock + try/finally automatiquement
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Ces trois mécanismes de synchronisation sont souvent confondus, bien qu'ils aient des propriétés et des cas d'utilisation différents. Mutex est binaire avec propriété. Le sémaphore est un compteur de permissions sans propriété. Le Monitor est un mécanisme de haut niveau combinant Mutex avec des variables de condition. Comprendre les différences est crucial pour choisir le bon outil pour une tâche spécifique.
| Paramètre | Mutex | Sémaphore | Monitor |
|---|---|---|---|
| Type | Binaire (0/1) | Compteur (0..N) | Binaire + conditions |
| Propriété | Seul le propriétaire peut unlock | N'importe quel thread peut signal | Seul le propriétaire |
| Réentrance | Généralement oui (reentrant) | Non | Oui |
| Attente conditionnelle | Non (nécessite Condition) | Non | Intégré (wait/notify) |
| Exemple en Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Quand choisir Mutex : vous devez protéger une seule ressource contre l'accès concurrent — par exemple, une collection partagée, un fichier ou un compteur. Quand choisir Sémaphore — vous devez limiter le nombre d'accès concurrents à un pool de ressources, comme un pool de connexions de base de données avec 5 connexions. Quand choisir Monitor — vous avez besoin de synchronisation avec attente conditionnelle, comme une file producteur-consommateur via wait/notify. Dans le développement Android moderne, synchronized est souvent remplacé par ReentrantLock ou kotlinx.coroutines Mutex.
L'erreur la plus courante est l'absence du bloc finally pour appeler unlock(). Si une exception survient dans la section critique, le Mutex reste verrouillé et les autres threads attendent indéfiniment. Même si vous êtes sûr que les exceptions sont impossibles — utilisez toujours try/finally ou withLock. C'est un principe de programmation défensive, particulièrement important dans le développement mobile où des exceptions peuvent survenir en raison d'un manque de mémoire ou de Configuration Changes.
Lorsqu'une application utilise plusieurs Mutex, il est crucial d'établir un ordre d'acquisition cohérent. Si le Thread A acquiert M1 → M2, et le Thread B acquiert M2 → M1, un Deadlock se produit. Dans les grands projets (plus de 50 mille lignes de code), l'ordre des verrous est documenté dans la décision d'architecture et vérifié par des linters. L'outil Lock Checker dans IntelliJ IDEA détecte automatiquement l'ordre d'acquisition des verrous incohérent.
Maintenir un Mutex pendant plus de 1-2 millisecondes est un signe de mauvaise conception. La section critique doit contenir uniquement les opérations minimales nécessaires. Les requêtes réseau, les entrées-sorties de fichiers et les calculs complexes doivent être effectués en dehors du bloc verrouillé. Dans Android, le maintien prolongé d'un verrou dans le thread UI entraîne des pertes d'images (jank) et des ANR. Utilisez ReadWriteLock si la section critique se compose principalement d'opérations de lecture.
La bibliothèque kotlinx.coroutines fournit sa propre implémentation de Mutex, qui diffère fondamentalement du ReentrantLock classique. La principale différence est que suspending Mutex ne bloque pas le thread OS mais suspend la coroutine jusqu'à ce que le verrou soit libéré. Cela signifie que le thread peut exécuter d'autres coroutines pendant que la coroutine actuelle attend le 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 — ne bloque pas le thread
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Caractéristiques clés de kotlinx Mutex : non réentrant (non-reentrant) — contrairement à ReentrantLock, une coroutine ne peut pas réacquérir un Mutex qu'elle possède déjà. Si cela est nécessaire, utilisez Semaphore(1) au lieu de Mutex. De plus, Mutex de kotlinx.coroutines est non bloquant : il utilise la suspension via suspend, ce qui lui permet de ne pas bloquer le thread du pool.
En pratique, suspending Mutex est préférable au ReentrantLock classique dans le code de coroutines pour deux raisons : évolutivité — une coroutine attend le Mutex pendant que le thread dessert d'autres coroutines, augmentant le débit du système ; pas de BlockedThread — aucune ressource n'est dépensée pour le stockage de la pile du thread bloqué. Selon JetBrains (Kotlin Coroutines Guide, 2024), l'utilisation de suspending Mutex améliore le débit de 40% avec 100+ coroutines.
Foire Aux Questions
La propriété (ownership) est la différence fondamentale. Mutex se souvient quel thread l'a acquis, et seul ce thread peut le libérer. Un sémaphore binaire (Semaphore(1)) n'a pas de propriétaire — n'importe quel thread peut appeler release(). Par conséquent, Mutex est plus sûr : un autre thread ne peut pas accidentellement libérer le verrou de quelqu'un d'autre, mais un sémaphore le peut.
synchronized est plus simple et plus court — utilisez-le pour des sections critiques simples sans timeouts ni contrôle d'équité. Utilisez ReentrantLock lorsque vous avez besoin de TryLock avec timeout, d'ordonnancement équitable, de Variables de Condition ou d'interruption du thread en attente (lockInterruptibly). Pour les coroutines, utilisez toujours kotlinx.coroutines.sync.Mutex.
Spinlock est un verrou où le thread ne dort pas mais tourne dans une boucle (spin) vérifiant l'état du verrou. Spinlock consomme du CPU mais ne change pas de contexte, ce qui le rend avantageux pour les sections critiques courtes (jusqu'à 10 instructions). Mutex place le thread dans l'état BLOCKED, ce qui coûte 10-50 microsecondes de plus en raison du changement de contexte, mais ne gaspille pas de CPU.
Au niveau du noyau Linux, Mutex est implémenté via futex (fast userspace mutex). Le thread essaie d'abord d'acquérir le verrou dans l'espace utilisateur via l'instruction atomique CAS (Compare-And-Swap). Si le Mutex est libre — l'acquisition se fait sans appel système. S'il est occupé — le thread effectue l'appel système futex(FUTEX_WAIT) et s'endort. Lors de la libération, l'appel système futex(FUTEX_WAKE) réveille un thread en attente.
Oui, il existe des Mutex inter-processus (inter-process mutex). Sous Windows, c'est Named Mutex ; sous Linux — pthread_mutexattr_setpshared avec l'attribut PTHREAD_PROCESS_SHARED. La Bionic libc d'Android supporte également les Mutex inter-processus via des descripteurs de fichiers. Les Mutex inter-processus sont utilisés pour la synchronisation entre différentes applications ou entre un processus et ses processus fils.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi