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.