Quesion about ConcurrentMap.getOrPut

Hi,

I have a question about ConcurrentMap.getOrPut function. How could it happen that
but the [defaultValue] function may be invoked even if the key is already in the map?

/**
 * Concurrent getOrPut, that is safe for concurrent maps.
 *
 * Returns the value for the given [key]. If the key is not found in the map, calls the [defaultValue] function,
 * puts its result into the map under the given key and returns it.
 *
 * This method guarantees not to put the value into the map if the key is already there,
 * but the [defaultValue] function may be invoked even if the key is already in the map.
 */
public inline fun <K, V> ConcurrentMap<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
    // Do not use computeIfAbsent on JVM8 as it would change locking behavior
    return this.get(key)
            ?: defaultValue().let { default -> this.putIfAbsent(key, default) ?: default }

}

I, in facts, came across this problem, the defaultValue function was invoked repeatedly when the key was in the map. But I can not figure it out. Finally I change the function to code:

fun myGetOrPut():Item {
  val cacheItem = cacheMap[id]
  if (cacheItem!=null) return cacheItem
  val item = defaultValue()
  cacheMap[id] = item
  return item
}

And these code run well, defaultValue was never invoked when the key was there

Concurrency.

Another thread set the same id while your thread is invoking defaultValue(), then your thread replaces the value of other thread.
At the end different threads are working with different values.

2 Likes

I came here searching for any explanation of this mysterious comment in JetBrains code:
Do not use computeIfAbsent on JVM8 as it would change locking behavior

So what’s wrong with computeIfAbsent? Is the problem only present on JVM 8? Can I use it on Jvm 17?

Is it actually true? I would assume the first computed value would prevail and both threads would use it, potentially ignoring their own computed value. Otherwise, this function would actually break its guarantee that the value is set only if absent. I believe it should be safe to assume that concurrent getOrPut() calls for the same key will always get exactly the same value.

This example seems to confirm such behavior:

fun main() {
    val map = ConcurrentHashMap<String, String>()
    
    thread {
        val value = map.getOrPut("foo") {
            Thread.sleep(500)
            "1"
        }
        println("1: $value")
    }
    thread {
        val value = map.getOrPut("foo") {
            Thread.sleep(1000)
            "2"
        }
        println("2: $value")
    }
}

Both threads print “1”.

Nothing’s wrong with computeIfAbsent in JVM 8+. The comment warns that delegating the implementation of getOrPut to computeIfAbsent would have an observable effect on the locking behavior.
Currently, getOrPut doesn’t lock the key, but can execute the provided defaultValue function multiple times and in parallel. On the contrary, some implementations of computeIfAbsent may do the operation atomically, i.e. at most once for each key.

Please consider that invoking concurrently the getOrPut extension method on a non thread-safe map can cause unexpected results, as a simple put does.

This method is indeed not thread-safe. Was expecting it to block the getOrPut operation while the lambda to get the default value is executed by another thread:


 which doesn’t work. So, what’s the whole point of this method then, why is it documented as ‘safe’? Seems true for the GET and PUT operation itself, but not for the whole (!) operation.

(Kotlin 1.7.10 on JVM 11)

I think by “safe” they mean after we compute the default, we check again if the key exists and we set the value in a safe way - using putIfAbsent(). In practice that means if we use getOrPut() concurrently (and optionally get() as well), it is guaranteed we can only get exactly the same value from all calls. That also means getOrPut() may return a different value than it produced inside defaultValue.

This is actually explained in docs:

This method guarantees not to put the value into the map if the key is already there, but the defaultValue function may be invoked even if the key is already in the map.