Scala, boxing and concurrent collections
I am working on concurrent Scala code. Native Scala collections do not have that many features, so I have to use native Java collections together with some extra libraries (Eclipse Collections, Guava). It is real pain to do use those from Scala, here is one example why you should just use Kotlin.
File reader count
I need to track number of readers for each file. it is simple semaphore system using: Map<File, Long>
, where key is file, value is number of readers.
Ideally this map should support atomic operations (cas
, swap
, computeIfAbsent
…). No locking should be required to maintain this map. So I need some sort of concurrent map.
Scala has scala.collection.mutable.ConcurrentMap
interface, but without any actual implementations. Plus it does not have any useful functions.
Java has java.util.concurrent.ConcurrentMap
with nice methods such as compute
, computeWithPresent
etc… So the choice is simple.
The map declaration looks like this:
// scala code
val fileSemaphore = new ConcurrentHashMap[File, Long]()
Lock
Now the locking method. It should increment value (number of readers) in atomic way. If key (File) is not in map (has zero readers), it should insert default value 1
// scala code
def fileLock(file:File){
fileSemaphore.compute(file, {(file,value)=>
if(value==null) 1
else value+1
})
}
- The
compute
method will update map in atomic way. - If the key-value pair is not present, the
value
argument will be null Function returns new value, which is inserted into map
- This code is actually broken and just happens to work (see next chapter)
Unlock
Now the file unlock method.
- It should decrement number of readers
- If the number of readers reaches zero, it should remove key from Map (function returns
null
)
// scala code
def fileUnlock(file:File): Unit ={
fileSemaphore.compute(file, {(file,value:Long) =>
if(value==1L) null
else value-1
})
}
This code does not compile, scalac can not infer function return type from null
. It need cast for extra hint:
if(value==1L) null.asInstanceOf[Long]
else value-1
It compiles and runs, but does not work. When number of readers reaches zero, the file is not removed from Map but its value is set to zero.
Nullable Long in Scala
Both lock and unlock methods are broken. If you convert this code to Java, it works. Problem is that Scala does not allow nullable longs. Any null
variable or expression with type Long
is silently converted to 0L
.
First lock
method just happens to work, because the non-existing key (null value) is silently converted to 0L and incremented. But the first line in this expression is never executed:
if(value==null) 1 // always false, never executed, value is never null, but 0L
else value+1
Unlock method never returns null (to remove file from map). First line is executed, but the return value is converted to 0L, and inserted to map:
if(value==1L) null.asInstanceOf[Long] //converted to 0L
else value-1
This conversion is pretty nasty. It swaps your values at runtime. I would expect something like that from Javascript, but not from strongly typed language.
To be fair the scalac emits warning in lock method, but it is not fatal error. And unlock method passes without warning.
Solution
I found it impossible to write correct solution in Scala. There are two workarounds:
Remove
Long
definition and useAny
to keep compiler away:Map[File, Long]
becomesMap[File,Any]
.Write lock/unlock methods in Java.
Nullable types in Kotlin
I could not resist to show how elegant this code becomes in Kotlin with nullable types. So I rewrote code above in Kotlin.
// kotlin code
val readers = ConcurrentHashMap<File,Long>()
fun lock(file:File):Unit{
readers.compute(file,{file, value ->
if(value==null) 1L
else value+1
})
}
fun unlock(file:File):Unit{
readers.compute(file,{file, value ->
if(value==1L) null
else value-1
})
}
Fancy null operators (elvis) would not make code better. Instead Kotlin provided another unexpected benefit; compiler found concurrency issue :-)
Code above does not compile. If we unlock wrong file (not yet locked), value is null and else value-1
would throw NPE.
So we need to handle case when wrong file is unlocked. This version is correct and compiles:
// kotlin code
fun unlock(file:File):Unit{
readers.compute(file,{file, value ->
if(value==null)
throw IllegalMonitorStateException("file not locked")
if(value==1L) null
else value-1
})
}
PS: Keep on mind that ConcurrentMap
is interface defined in Java code. Nullability information was added latter with external annotations :-)
Comments
Avatar
Tse-Wen Wang (Tom) • 3 years ago • edited
Scala supports nullable Long through its support of Java classes. Here's how I would use nullable Long.
import java.lang.{Long => JLong} // JLong will be alias for java.lang.Long
null.asInstanceOf[JLong] // returns null
--
Mateusz Maciaszek • 4 years ago
Wouldn't using AtomicRef with Optional type help to resolve all of these problems?
Avatar
Jan Kotek Mateusz Maciaszek • 4 years ago
I guess you mean in combination with immutable Scala Map. It would not, it generates too much GC garbage.
−
Avatar
Mateusz Maciaszek Jan Kotek • 4 years ago
Not really, mutable version should be ok as well once dealing with concurrency control mechanism (hence not sure about GC pressure).