Mutex (mutual exclusion) — ay isang synchronization primitive na ginagarantiyang isang thread lamang ang maaaring mag-execute ng kritikal na seksyon ng code sa bawat sandali. Ayon sa Microsoft Docs (Synchronization Objects, 2024), ang pangunahing prinsipyo ng Mutex ay pagmamay-ari: ang thread na nakakuha ng Mutex ay nagiging may-ari nito at pinapalaya lamang ito kapag lumabas sa kritikal na seksyon. Mutex — isang pangunahing kasangkapan para maiwasan ang Race Condition at matiyak ang integridad ng datos sa mga multi-threaded application.
Mga Pangunahing Punto
Mutex (pagdadaglat ng Mutual Exclusion — mutual exclusion) — ay isang synchronization object na namamahala ng access sa shared resource sa isang multi-threaded na kapaligiran. Kapag ang isang thread ay pumasok sa kritikal na seksyon, kinukuha nito ang Mutex. Kung ang isa pang thread ay sumubok na kunin ang parehong Mutex, ito ay ililipat sa estado ng paghihintay hanggang sa mapalaya ang lock ng unang thread.
Ang arkitektura ng Mutex ay nagmula sa operating system na THE, na binuo ni Edsger Dijkstra noong 1965. Si Dijkstra ang nagpakilala ng konsepto ng mga semaphore, kung saan kalaunan ay nahiwalay ang Mutex bilang isang espesyal na kaso — binary semaphore na may suporta sa pagmamay-ari. Ang mga modernong OS (Linux, Windows, Android) ay nagpapatupad ng Mutex sa kernel level, na tinitiyak ang tamang synchronization kahit sa pagitan ng iba't ibang proseso.
Ang pangunahing pag-aari ng Mutex ay ownership (pagmamay-ari). Tanging ang thread na kumuha ng mutex ang makapagpapalaya nito. Ito ang nagpapakilala ng Mutex mula sa binary semaphore, kung saan ang anumang thread ay maaaring mag-execute ng signal (V-operation). Ang pagmamay-ari ay pumipigil sa aksidenteng pagpapalaya ng lock ng ibang thread, na ginagawang mas ligtas ang Mutex para sa tipikal na mga synchronization scenario sa mobile development. Ayon sa Android Developer Docs (Processes and Threads, 2024), ang paggamit ng Mutex sa halip na synchronized ay maaaring magpataas ng performance ng 30% sa mataas na kompetisyon.
Ang Mutex ay nasa isa sa dalawang estado: naka-lock (locked) — kinuha ng isang thread; libre (unlocked) — hindi kinuha. Dalawang pangunahing operasyon — lock() (pagkuha) at unlock() (pagpapalaya). Kung ang Mutex ay nakuha na, ang thread na tumatawag ng lock() ay haharang hanggang sa mapalaya. Sa JVM, ang naka-block na thread ay pumapasok sa estado ng BLOCKED at hindi gumagamit ng CPU.
Kapag ang Mutex ay pinalaya, pinipili ng system kung alin sa naghihintay na mga thread ang makakakuha ng lock. Sa hindi patas (non-fair) na pag-iiskedyul ang pagpili ay maaaring mahulog sa thread na kakatapos lang magpalaya ng mutex — pinapataas nito ang throughput, ngunit maaaring humantong sa Starvation (gutom). Ang patas (fair) na scheduler ay gumagamit ng FIFO queue: ang unang naghihintay na thread ay unang makakakuha ng lock. Ang ReentrantLock(true) ay nagpapatupad ng mekanismong ito.
Karamihan sa mga implementasyon ng Mutex sa Java/Kotlin ay sumusuporta sa recursive (reentrant) na pagkuha. Kung ang isang thread ay may-ari na ng Mutex at muling tumawag ng lock(), ang operasyon ay matagumpay — hindi hinaharangan ng Mutex ang sarili nito. Ang recursion counter ay tumataas, at ang thread ay dapat tumawag ng unlock() nang kasing-dami ng lock(). Ito ay mahalaga para sa recursive na mga tawag at nested na kritikal na mga seksyon.
Isaalang-alang natin ang isang tipikal na gawain — pagprotekta ng shared counter laban sa Race Condition gamit ang ReentrantLock (klasikong Mutex sa Java/Kotlin). Kung walang Mutex, ang code ay magbibigay ng maling resulta; sa Mutex, lahat ng 1000 thread ay garantisadong tataas ang halaga ng counter.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // kritikal na seksyon
} finally {
mutex.unlock() // mandatoryong 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()) // Laging 1000
}
Pansinin ang finally block — mandatoryong pattern kapag nagtatrabaho sa Mutex. Kung sa loob ng kritikal na seksyon ay may exception, hindi tatawagin ang unlock(), at ang Mutex ay mananatiling naka-lock magpakailanman — ito ay humahantong sa Deadlock. Ginagarantiya ng finally block ang pagpapalaya ng Mutex sa anumang kinalabasan ng pag-execute ng seksyon.
Alternatibong diskarte sa Kotlin — paggamit ng extension function na withLock, na awtomatikong humahawak ng lock/unlock na may finally.
fun increment() {
mutex.withLock { // lock + try/finally awtomatiko
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Ang tatlong synchronization mechanism na ito ay madalas na pinagkakamalan, bagama't may iba't ibang mga pag-aari at lugar ng aplikasyon. Mutex — binary, na may pagmamay-ari. Semaphore — counter ng mga pahintulot, walang pagmamay-ari. Monitor — mekanismo sa mataas na antas na pinagsasama ang Mutex sa mga condition variable. Ang pag-unawa sa mga pagkakaiba ay kritikal para sa pagpili ng tamang kasangkapan para sa isang partikular na gawain.
| Parameter | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Uri | Binary (0/1) | Nabibilang (0..N) | Binary + kondisyon |
| Pagmamay-ari | Tanging may-ari ang makaka-unlock | Anumang thread ay makaka-signal | Tanging may-ari |
| Recursivity | Karaniwan oo (reentrant) | Hindi | Oo |
| May kundisyong paghihintay | Hindi (kailangan ng Condition) | Hindi | Built-in (wait/notify) |
| Halimbawa sa Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Kailan pumili ng Mutex: kailangang protektahan ang isang resource mula sa sabay-sabay na pag-access — halimbawa, shared collection, file, o counter. Kailan pumili ng Semaphore — kailangang limitahan ang bilang ng sabay-sabay na pag-access sa isang pool ng mga resource, halimbawa database connection pool na may 5 koneksyon. Kailan pumili ng Monitor — kailangan ang synchronization na may kondisyong paghihintay, halimbawa producer-consumer queue sa pamamagitan ng wait/notify. Sa modernong Android development, ang synchronized ay madalas na pinapalitan ng ReentrantLock o kotlinx.coroutines Mutex.
Ang pinakakaraniwang pagkakamali — kawalan ng finally block para tawagin ang unlock(). Kung sa kritikal na seksyon ay may exception, mananatiling naka-lock ang Mutex, at ang ibang mga thread ay maghihintay magpakailanman. Kahit na sigurado kang imposible ang mga exception — palaging gumamit ng try/finally o withLock. Ito ay prinsipyo ng defensive programming, lalong mahalaga sa mobile development, kung saan ang mga exception ay maaaring mangyari dahil sa kakulangan ng memory o Configuration Changes.
Kapag sa isang application ay ginagamit ang maraming Mutex, kritikal na magtatag ng pare-parehong pagkakasunod-sunod ng pagkuha ng mga ito. Kung ang Thread A ay kukuha ng M1 → M2, at ang Thread B ay kukuha ng M2 → M1, magkakaroon ng Deadlock. Sa malalaking proyekto (mahigit sa 50 libong linya ng code), ang pagkakasunod-sunod ng mga lock ay idodokumento sa desisyon ng arkitektura at susuriin ng mga linter. Ang Lock Checker tool sa IntelliJ IDEA ay awtomatikong nakakatuklas ng hindi pare-parehong pagkakasunod-sunod ng pagkuha ng lock.
Ang paghawak ng Mutex nang higit sa 1-2 millisecond — tanda ng maling disenyo. Ang kritikal na seksyon ay dapat maglaman lamang ng minimal na kinakailangang operasyon. Ang mga network request, file I/O at kumplikadong kalkulasyon ay dapat i-execute sa labas ng naka-lock na block. Sa Android, ang mahabang paghawak ng lock sa UI thread ay humahantong sa paglaktaw ng frame (jank) at ANR. Gumamit ng ReadWriteLock kung ang kritikal na seksyon ay pangunahing binubuo ng read operations.
Ang library na kotlinx.coroutines ay nagbibigay ng sarili nitong implementasyon ng Mutex, na pangunahing naiiba mula sa klasikong ReentrantLock. Ang pangunahing pagkakaiba — ang suspending Mutex ay hindi hinaharangan ang OS thread, kundi sine-suspend ang coroutine hanggang sa mapalaya ang lock. Ito ay nangangahulugan na ang thread ay maaaring mag-execute ng iba pang coroutine habang ang kasalukuyang coroutine ay naghihintay ng 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 — hindi hinaharangan ang thread
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Mga pangunahing katangian ng kotlinx Mutex: non-reentrant — hindi tulad ng ReentrantLock, ang isang coroutine ay hindi maaaring muling kunin ang Mutex na pagmamay-ari na nito. Kung kinakailangan, gumamit ng Semaphore(1) sa halip na Mutex. Bukod pa rito, ang Mutex mula sa kotlinx.coroutines ay non-blocking: gumagamit ito ng pagsususpend sa pamamagitan ng suspend, na nagpapahintulot na huwag harangin ang pool thread.
Sa praktika, ang suspending Mutex ay mas gusto kaysa sa klasikong ReentrantLock sa coroutine code para sa dalawang dahilan: scalability — isang coroutine ang naghihintay ng Mutex, habang ang thread ay naglilingkod sa iba pang coroutine, na nagpapataas ng throughput ng system; kawalan ng BlockedThread — walang nasasayang na resource para sa pag-imbak ng stack ng naka-block na thread. Ayon sa JetBrains (Kotlin Coroutines Guide, 2024), ang paggamit ng suspending Mutex ay nagpapataas ng throughput ng 40% sa 100+ coroutine.
Mga Madalas Itanong
Pagmamay-ari (ownership) — ang pangunahing pagkakaiba. Naaalala ng Mutex kung aling thread ang kumuha nito at tanging thread na iyon ang makapagpapalaya nito. Ang binary semaphore (Semaphore(1)) ay walang may-ari — anumang thread ay maaaring mag-execute ng release(). Kaya ang Mutex ay mas ligtas: ang ibang thread ay hindi maaaring aksidenteng magpalaya ng lock ng iba, samantalang ang semaphore ay maaari.
synchronized ay mas simple at mas maikli — gamitin ito para sa simpleng kritikal na mga seksyon na walang timeout at walang kontrol sa pagiging patas. Gamitin ang ReentrantLock kapag kailangan ang TryLock na may timeout, fair-planning, Condition Variables o pag-interrupt sa naghihintay na thread (lockInterruptibly). Para sa coroutine, palaging gamitin ang kotlinx.coroutines.sync.Mutex.
Spinlock — ay isang lock kung saan ang thread ay hindi natutulog, kundi sa isang loop (spin) sinusuri ang estado ng lock. Ang Spinlock ay gumagamit ng CPU, ngunit hindi nag-switch ng konteksto, na ginagawang kapaki-pakinabang para sa maiikling kritikal na seksyon (hanggang 10 instruction). Ang Mutex ay naglilipat ng thread sa estado ng BLOCKED, na mas mahal ng 10-50 microsecond dahil sa context switch, ngunit hindi gumagamit ng CPU.
Sa antas ng kernel ng Linux, ang Mutex ay ipinatupad sa pamamagitan ng futex (fast userspace mutex). Unang sinusubukan ng thread na kunin ang lock sa userspace sa pamamagitan ng atomic CAS instruction (Compare-And-Swap). Kung libre ang Mutex — ang pagkuha ay nangyayari nang walang syscall. Kung abala — ang thread ay gumagawa ng syscall futex(FUTEX_WAIT) at natutulog. Sa pagpapalaya, ang syscall futex(FUTEX_WAKE) ay gumigising ng isang naghihintay na thread.
Oo, mayroong inter-process Mutex (inter-process mutex). Sa Windows ito ay Named Mutex, sa Linux — pthread_mutexattr_setpshared na may attribute na PTHREAD_PROCESS_SHARED. Sa Android, sinusuportahan din ng Bionic libc ang inter-process Mutex sa pamamagitan ng file descriptor. Ang inter-process Mutex ay ginagamit para sa synchronization sa pagitan ng iba't ibang application o sa pagitan ng isang proseso at mga child process nito.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din