Mutex (esclusione reciproca) è una primitiva di sincronizzazione che garantisce che un solo thread possa eseguire una sezione critica di codice in un dato momento. Secondo Microsoft Docs (Synchronization Objects, 2024), il principio fondamentale di Mutex è la proprietà: un thread che acquisisce un Mutex ne diventa il proprietario e lo rilascia solo all'uscita dalla sezione critica. Mutex è uno strumento fondamentale per prevenire le Race Condition e garantire l'integrità dei dati nelle applicazioni multithread.
Punti Chiave
Mutex (abbreviazione di Mutual Exclusion — esclusione reciproca) è un oggetto di sincronizzazione che gestisce l'accesso a una risorsa condivisa in un ambiente multithread. Quando un thread entra in una sezione critica, acquisisce il Mutex. Se un altro thread tenta di acquisire lo stesso Mutex, viene messo in stato di attesa fino a quando il lock non viene rilasciato dal primo thread.
L'architettura di Mutex risale al sistema operativo THE, progettato da Edsger Dijkstra nel 1965. Dijkstra introdusse il concetto di semafori, da cui Mutex emerse successivamente come caso speciale — un semaforo binario con supporto alla proprietà. I SO moderni (Linux, Windows, Android) implementano Mutex a livello di kernel, garantendo una sincronizzazione corretta anche tra processi diversi.
La proprietà chiave di Mutex è la proprietà (ownership). Solo il thread che ha acquisito il mutex può rilasciarlo. Questo distingue Mutex da un semaforo binario, dove qualsiasi thread può eseguire un segnale (operazione V). La proprietà impedisce il rilascio accidentale del lock da parte di un altro thread, rendendo Mutex più sicuro per scenari tipici di sincronizzazione nello sviluppo mobile. Secondo Android Developer Docs (Processes and Threads, 2024), l'uso di Mutex invece di synchronized può migliorare le prestazioni del 30% in condizioni di alta contesa.
Un Mutex si trova in uno di due stati: bloccato (locked) — acquisito da un thread; o libero (unlocked) — non acquisito. Esistono due operazioni di base: lock() (acquisire) e unlock() (rilasciare). Se il Mutex è già bloccato, il thread che chiama lock() viene bloccato fino al rilascio del lock. Nella JVM, un thread bloccato passa allo stato BLOCKED e non consuma CPU.
Quando un Mutex viene rilasciato, il sistema seleziona quale thread in attesa riceve il lock. Con pianificazione non equa (non-fair), la scelta può cadere sul thread che ha appena rilasciato il mutex — questo aumenta la produttività ma può portare all'inedia (Starvation). Un pianificatore equo (fair) utilizza una coda FIFO: il primo thread in attesa riceve il lock per primo. ReentrantLock(true) implementa esattamente questo meccanismo.
La maggior parte delle implementazioni di Mutex in Java/Kotlin supportano l'acquisizione rientrante. Se un thread possiede già il Mutex e chiama di nuovo lock(), l'operazione ha successo — il Mutex non blocca se stesso. Il contatore di ricorsione aumenta e il thread deve chiamare unlock() tante volte quante lock(). Questo è importante per chiamate ricorsive e sezioni critiche annidate.
Consideriamo un compito tipico — proteggere un contatore condiviso dalle Race Condition usando ReentrantLock (Mutex classico in Java/Kotlin). Senza Mutex, il codice produrrebbe risultati errati; con Mutex, tutti i 1000 thread incrementano in modo affidabile il valore del contatore.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // sezione critica
} finally {
mutex.unlock() // finally obbligatorio
}
}
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()) // Sempre 1000
}
Presta attenzione al blocco finally — un pattern obbligatorio quando si lavora con Mutex. Se si verifica un'eccezione all'interno della sezione critica, unlock() non verrà chiamato e il Mutex rimarrà bloccato per sempre — questo porta a un Deadlock. Il blocco finally garantisce il rilascio del Mutex indipendentemente da come termina l'esecuzione della sezione.
Un approccio alternativo in Kotlin è l'uso della funzione di estensione withLock, che gestisce automaticamente lock/unlock con finally.
fun increment() {
mutex.withLock { // lock + try/finally automaticamente
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Questi tre meccanismi di sincronizzazione sono spesso confusi, sebbene abbiano proprietà e casi d'uso diversi. Mutex è binario con proprietà. Il Semaforo è un contatore di permessi senza proprietà. Il Monitor è un meccanismo di alto livello che combina Mutex con variabili di condizione. Comprendere le differenze è criticamente importante per scegliere lo strumento giusto per un compito specifico.
| Parametro | Mutex | Semaforo | Monitor |
|---|---|---|---|
| Tipo | Binario (0/1) | Contatore (0..N) | Binario + condizioni |
| Proprietà | Solo il proprietario può unlock | Qualsiasi thread può signal | Solo il proprietario |
| Ri entranza | Di solito sì (reentrant) | No | Sì |
| Attesa condizionale | No (necessita Condition) | No | Integrata (wait/notify) |
| Esempio in Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Quando scegliere Mutex: devi proteggere una singola risorsa dall'accesso concorrente — ad esempio, una collezione condivisa, un file o un contatore. Quando scegliere Semaforo — devi limitare il numero di accessi concorrenti a un pool di risorse, come un pool di connessioni al database con 5 connessioni. Quando scegliere Monitor — hai bisogno di sincronizzazione con attesa condizionale, come una coda produttore-consumatore tramite wait/notify. Nello sviluppo Android moderno, synchronized è spesso sostituito da ReentrantLock o kotlinx.coroutines Mutex.
L'errore più comune è l'assenza del blocco finally per chiamare unlock(). Se si verifica un'eccezione nella sezione critica, il Mutex rimane bloccato e altri thread aspettano per sempre. Anche se sei sicuro che le eccezioni siano impossibili — usa sempre try/finally o withLock. Questo è un principio di programmazione difensiva, particolarmente importante nello sviluppo mobile dove le eccezioni possono sorgere a causa di mancanza di memoria o Configuration Changes.
Quando un'applicazione utilizza più Mutex, è criticamente importante stabilire un ordine di acquisizione coerente. Se il Thread A acquisisce M1 → M2 e il Thread B acquisisce M2 → M1, si verifica un Deadlock. Nei progetti grandi (oltre 50 mila righe di codice), l'ordine dei lock è documentato nella decisione architetturale e verificato dai linter. Lo strumento Lock Checker in IntelliJ IDEA rileva automaticamente l'ordine di acquisizione dei lock incoerente.
Mantenere un Mutex per più di 1-2 millisecondi è segno di una progettazione errata. La sezione critica dovrebbe contenere solo le operazioni minime necessarie. Le richieste di rete, l'I/O di file e i calcoli complessi dovrebbero essere eseguiti al di fuori del blocco bloccato. In Android, il mantenimento prolungato di un lock nel thread dell'UI porta alla perdita di frame (jank) e ANR. Usa ReadWriteLock se la sezione critica consiste principalmente di operazioni di lettura.
La libreria kotlinx.coroutines fornisce la propria implementazione di Mutex, che differisce fondamentalmente dal classico ReentrantLock. La differenza principale è che suspending Mutex non blocca il thread del SO ma sospende la coroutine fino al rilascio del lock. Ciò significa che il thread può eseguire altre coroutine mentre quella corrente attende il 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 — non blocca il thread
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Caratteristiche principali di kotlinx Mutex: non rientrante (non-reentrant) — a differenza di ReentrantLock, una coroutine non può riacquisire un Mutex che già possiede. Se necessario, usa Semaphore(1) invece di Mutex. Inoltre, Mutex di kotlinx.coroutines è non bloccante: utilizza la sospensione tramite suspend, permettendo di non bloccare il thread del pool.
In pratica, suspending Mutex è preferibile al classico ReentrantLock nel codice delle coroutine per due ragioni: scalabilità — una coroutine attende il Mutex mentre il thread serve altre coroutine, aumentando la produttività del sistema; nessun BlockedThread — non si spreca risorsa per memorizzare lo stack del thread bloccato. Secondo JetBrains (Kotlin Coroutines Guide, 2024), l'uso di suspending Mutex migliora la produttività del 40% con 100+ coroutine.
Domande Frequenti
La proprietà (ownership) è la differenza fondamentale. Mutex ricorda quale thread lo ha acquisito e solo quel thread può rilasciarlo. Un semaforo binario (Semaphore(1)) non ha proprietario — qualsiasi thread può chiamare release(). Pertanto, Mutex è più sicuro: un altro thread non può rilasciare accidentalmente il lock di qualcun altro, ma un semaforo può.
synchronized è più semplice e breve — usalo per sezioni critiche semplici senza timeout e controllo di equità. Usa ReentrantLock quando hai bisogno di TryLock con timeout, pianificazione equa, Variabili di Condizione o interruzione del thread in attesa (lockInterruptibly). Per le coroutine, usa sempre kotlinx.coroutines.sync.Mutex.
Spinlock è un lock dove il thread non dorme ma gira in un ciclo (spin) controllando lo stato del lock. Spinlock consuma CPU ma non cambia contesto, rendendolo vantaggioso per sezioni critiche brevi (fino a 10 istruzioni). Mutex mette il thread nello stato BLOCKED, che costa 10-50 microsecondi in più a causa del cambio di contesto, ma non spreca CPU.
A livello di kernel Linux, Mutex è implementato tramite futex (fast userspace mutex). Il thread prima tenta di acquisire il lock in userspace tramite l'istruzione atomica CAS (Compare-And-Swap). Se il Mutex è libero — l'acquisizione avviene senza syscall. Se è occupato — il thread esegue la syscall futex(FUTEX_WAIT) e si addormenta. Al rilascio, la syscall futex(FUTEX_WAKE) risveglia un thread in attesa.
Sì, esistono Mutex inter-processo (inter-process mutex). In Windows è Named Mutex, in Linux — pthread_mutexattr_setpshared con l'attributo PTHREAD_PROCESS_SHARED. La Bionic libc di Android supporta anche Mutex inter-processo tramite descrittori di file. I Mutex inter-processo sono usati per la sincronizzazione tra diverse applicazioni o tra un processo e i suoi processi figli.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche