Search code examples
javascalajava.util.concurrentconcurrenthashmap

Why does Scala ConcurrentMap.putIfAbsent always try to put?


The ConcurrentMap section in the docs claim:

m putIfAbsent(k, v) Adds key/value binding k -> v unless k is already defined in m

but in reality, I found putIfAbsent always tries to insert v. The way to get around this is to use getOrElseUpdate, but is this really a bug, or am I missing something?

My code is the following:

val instanceMap: ConcurrentMap[Address, (MongodExecutable, MongodProcess)] =
    (new JavaConcurrentMap[Address, (MongodExecutable, MongodProcess)]()).asScala

def start(host: String = DefaultAddress.host, port: Int = DefaultAddress.port): MongodProcess = {
  val addr = Address(host, port)
  instanceMap.putIfAbsent(addr, { // this block is ALWAYS executed
    val mongodExecutable = starter.prepare(mongodConfig(host, port))
    val mongod = mongodExecutable.start()
    logProcessInfo(mongod)

    (mongodExecutable, mongod)
  })

  instanceMap(addr)
    ._2
}

Solution

  • putIfAbsent does evaluate the value of k, v because value is not a function/lambda.

    Example below

    original concurrent map

    scala> import java.util.concurrent.ConcurrentHashMap
    import java.util.concurrent.ConcurrentHashMap
    
    scala> new ConcurrentHashMap[String, String]()
    res0: java.util.concurrent.ConcurrentHashMap[String,String] = {}
    

    insert key1 - successful insertion returns null

    scala> res0.putIfAbsent("127.0.0.1", "mongod-process1")
    res1: String = null
    
    scala> res0
    res2: java.util.concurrent.ConcurrentHashMap[String,String] = {127.0.0.1=mongod-process1}
    

    insert key2 - successful insertion returns null

    scala> res0.putIfAbsent("127.0.0.2", "mongod-process2")
    res3: String = null
    
    scala> res0
    res4: java.util.concurrent.ConcurrentHashMap[String,String] = {127.0.0.2=mongod-process2, 127.0.0.1=mongod-process1}
    

    try to insert key1 again - failed insertion returns value1

    scala> res0.putIfAbsent("127.0.0.1", { println("evaluating mongod-process"); "mongod-process1-again" } )
    evaluating mongod-process
    res7: String = mongod-process1
    

    final map

    scala> res0
    res8: java.util.concurrent.ConcurrentHashMap[String,String] = {127.0.0.2=mongod-process2, 127.0.0.1=mongod-process1}
    

    Also, note putIfAbsent has null check on key value at the beginning(ConcurrentHashMap.java:1011) so which will have to evaluate the value anyway.

    scala> res0.putIfAbsent("127.0.0.1", { println("evaluating mongod-process"); null } )
    evaluating mongod-process
    java.lang.NullPointerException
      at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
      at java.util.concurrent.ConcurrentHashMap.putIfAbsent(ConcurrentHashMap.java:1535)
      ... 29 elided