My getResource
method below needs to cache a single resource, but it has a clear race condition. If multiple threads call it simultaneously, the expensive createResource
function can be called more than once, leading to an incorrect state.
How can this code be modified to be thread-safe and guarantee createResource
is only called once when needed? What are the standard Kotlin patterns to solve this, and what are their trade-offs?
// A simple data class representing the resource we want to cache.
data class CachedResource(val id: String)
/**
* This class has a race condition.
*/
class ResourceProvider {
// The shared mutable state that needs protection.
private var cachedResource: CachedResource? = null
// This simulates an expensive operation that we want to avoid calling unnecessarily.
private fun createResource(id: String): CachedResource {
println("Creating new resource for id: $id...")
// In a real app, this might be a heavy database call or network request.
Thread.sleep(1000) // Simulate work
return CachedResource(id)
}
/**
* This method is NOT thread-safe.
* How can I fix the race condition inside?
*/
fun getResource(id: String): CachedResource {
val localCache = cachedResource
// CHECK: If the cache is valid, return it.
if (localCache != null && localCache.id == id) {
return localCache
}
// ACT: If not, create and update the cache.
// This is the problematic section.
val newResource = createResource(id)
cachedResource = newResource
return newResource
}
}
If Java lock primitives are an option, you can use ReentrantLock
(javadoc link). In this case the code will look like this:
import java.util.concurrent.locks.ReentrantLock
data class CachedResource(val id: String)
class ResourceProvider {
private var cachedResource: CachedResource? = null
private val lock: ReentrantLock = ReentrantLock()
private fun createResource(id: String): CachedResource {
println("Creating new resource for id: $id...")
Thread.sleep(1_000) // Simulate work
return CachedResource(id)
}
fun getResource(id: String): CachedResource {
lock.lock()
try {
if (cachedResource == null || cachedResource!!.id != id) {
cachedResource = createResource(id)
}
return cachedResource!!
} finally {
lock.unlock()
}
}
}
An alternative implementation based on @Slaw 's comment:
import kotlin.concurrent.withLock
import java.util.concurrent.locks.ReentrantLock
...
fun getResource(id: String): CachedResource {
lock.withLock {
if (cachedResource == null || cachedResource!!.id != id) {
cachedResource = createResource(id)
}
return cachedResource!!
}
}