Comprendre Mutex, Lock, Semaphore et ReentrantLock en Kotlin (version très simple)
Quand on développe une application, plusieurs morceaux de code peuvent vouloir faire des choses en même temps. Par exemple : modifier une liste, incrémenter un compteur, écrire dans un fichier, mettre à jour un cache…
Le problème : si deux actions modifient la même donnée au même moment, on peut obtenir :
- des résultats incohérents (compteur faux),
- des données perdues,
- des bugs “aléatoires” difficiles à reproduire.
Les outils comme Mutex, Lock et Semaphore servent à une seule chose : éviter que plusieurs personnes touchent à la même chose en même temps.
1) L’idée de base : “une ressource partagée”
Imagine une ressource partagée :
- une salle de bain dans une colocation,
- une imprimante au bureau,
- une caisse au supermarché,
- ou une clé qui ouvre un seul local.
Si tout le monde essaie d’utiliser cette ressource en même temps, c’est le chaos. En code, c’est pareil : une donnée partagée doit être protégée.
2) Lock / Mutex : “une seule personne à la fois”
Un lock (ou un mutex) est comme une salle de bain :
- 1 personne entre,
- la porte se verrouille,
- les autres attendent,
- quand la personne sort, la suivante peut entrer.
En pratique, un mutex sert quand :
- une seule modification à la fois est autorisée,
- et que le moindre “mélange” casserait le résultat.
Exemple simple : un compteur partagé (si deux personnes incrémentent en même temps, on peut perdre des +1).
// Exemple très simple : on protège une action
val lock = Any()
fun increment() {
synchronized(lock) {
// Ici, une seule personne à la fois peut exécuter ce bloc
// compteur++
}
}
Tu n’as pas besoin de comprendre chaque ligne : l’idée est juste “un seul à la fois dans la zone protégée”.
3) synchronized vs ReentrantLock : deux façons de faire “la même porte”
En Kotlin/JVM, il y a deux manières courantes de faire un “verrou” :
- synchronized : la version la plus simple.
- ReentrantLock : une version plus “avancée” avec plus d’options.
3.1 synchronized (simple)
synchronized = “tu entres dans la pièce, tu sors, c’est fini”. C’est simple et très courant.
val lock = Any()
fun updateSharedData() {
synchronized(lock) {
// Zone protégée
}
}
3.2 ReentrantLock (plus d’options)
ReentrantLock fait la même chose, mais il permet aussi :
- tester si la porte est libre sans attendre (“si c’est occupé je passe mon tour”),
- attendre seulement un certain temps (“j’attends 2 secondes puis j’abandonne”).
val lock = java.util.concurrent.locks.ReentrantLock()
fun updateSharedData() {
lock.lock()
try {
// Zone protégée
} finally {
lock.unlock()
}
}
L’idée à retenir : ReentrantLock = même concept, mais plus de contrôle.
4) Pourquoi “Reentrant” ? (réentrant)
“Réentrant” signifie :
- si la même personne a déjà la clé, elle peut rentrer à nouveau sans se bloquer elle-même.
C’est utile quand un code appelle un autre code qui essaye aussi de prendre la même serrure.
5) Semaphore : “jusqu’à N personnes en même temps”
Un semaphore n’est pas “une seule personne à la fois”. C’est plutôt :
- “jusqu’à 3 personnes peuvent entrer”
- “au-delà, on attend”
Analogie : une petite salle avec 3 places. Quand les 3 places sont prises, les autres attendent.
En code, un semaphore sert par exemple à :
- limiter le nombre de requêtes réseau en parallèle,
- limiter le nombre de tâches lourdes en même temps,
- protéger un service qui ne supporte pas trop de charge.
val semaphore = java.util.concurrent.Semaphore(3) // 3 accès en même temps
fun callApi() {
semaphore.acquire()
try {
// Travail (appel réseau, traitement lourd, etc.)
} finally {
semaphore.release()
}
}
Idée à retenir : mutex/lock = 1 à la fois, semaphore = N à la fois.
6) Mutex (coroutines) : même idée, mais “sans bloquer”
Kotlin a aussi les coroutines (une façon moderne de faire de l’asynchrone).
Avec les coroutines, un Mutex sert à la même chose : une seule coroutine à la fois dans une zone critique.
La grande différence (sans entrer dans la technique) :
- un lock classique fait “attendre en bloquant”,
- un mutex coroutines fait “attendre intelligemment” (sans immobiliser le système).
val mutex = kotlinx.coroutines.sync.Mutex()
suspend fun updateSharedData() {
mutex.lock()
try {
// Zone protégée
} finally {
mutex.unlock()
}
}
Si tu utilises beaucoup de coroutines, c’est souvent le bon outil.
7) Avantages / inconvénients (ultra simple)
Lock / Mutex (1 à la fois)
- Avantage : protège très bien une donnée sensible.
- Inconvénient : si la zone protégée est longue, tout le monde attend longtemps.
ReentrantLock (lock avec options)
- Avantage : plus d’options (ne pas attendre, timeout).
- Inconvénient : un peu plus “verbeux” (plus de code).
Semaphore (N à la fois)
- Avantage : contrôle facilement la charge (ex : max 4 appels réseau).
- Inconvénient : si tu oublies de “rendre la place”, tout se bloque.
8) Quand utiliser quoi ? (règles simples)
- Tu veux empêcher 2 modifications en même temps ? Lock / Mutex.
- Tu veux juste limiter à N actions en parallèle ? Semaphore.
- Tu veux pouvoir abandonner si c’est bloqué ? ReentrantLock (try/timeout).
- Tu es dans un monde coroutines ? Mutex coroutines.
9) Le piège principal : oublier de libérer
Le bug le plus fréquent avec ces outils, c’est :
- prendre la clé,
- puis oublier de la rendre.
C’est pour ça qu’on met presque toujours le “release/unlock” dans un finally
(ou qu’on utilise des helpers qui le font automatiquement).
Conclusion
Si tu retiens une seule chose :
- Lock / Mutex = “1 seul à la fois”.
- Semaphore = “jusqu’à N en même temps”.
- ReentrantLock = “lock + options”.
- Mutex coroutines = “mutex adapté aux coroutines”.
Ces outils servent surtout à éviter les bugs invisibles et aléatoires quand plusieurs actions veulent toucher à la même donnée en même temps.