Skip to content

Commit da2d4ca

Browse files
committed
Fix SI-7943 -- make TrieMap.getOrElseUpdate atomic.
Override `getOrElseUpdate` method in `TrieMap`. The signature and contract of this method corresponds closely to the `computeIfAbsent` from `java.util.concurrent.ConcurrentMap`. Eventually, `computeIfAbsent` should be added to `scala.collection.concurrent.Map`. Add tests. Review by @Ichoran
1 parent d14e065 commit da2d4ca

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

src/library/scala/collection/concurrent/TrieMap.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,44 @@ extends scala.collection.concurrent.Map[K, V]
873873
insertifhc(k, hc, v, INode.KEY_ABSENT)
874874
}
875875

876+
// TODO once computeIfAbsent is added to concurrent.Map,
877+
// move the comment there and tweak the 'at most once' part
878+
/** If the specified key is not already in the map, computes its value using
879+
* the given thunk `op` and enters it into the map.
880+
*
881+
* Since concurrent maps cannot contain `null` for keys or values,
882+
* a `NullPointerException` is thrown if the thunk `op`
883+
* returns `null`.
884+
*
885+
* If the specified mapping function throws an exception,
886+
* that exception is rethrown.
887+
*
888+
* Note: This method will invoke op at most once.
889+
* However, `op` may be invoked without the result being added to the map if
890+
* a concurrent process is also trying to add a value corresponding to the
891+
* same key `k`.
892+
*
893+
* @param k the key to modify
894+
* @param op the expression that computes the value
895+
* @return the newly added value
896+
*/
897+
override def getOrElseUpdate(k: K, op: =>V): V = {
898+
val oldv = lookup(k)
899+
if (oldv != null) oldv.asInstanceOf[V]
900+
else {
901+
val v = op
902+
if (v == null) {
903+
throw new NullPointerException("Concurrent TrieMap values cannot be null.")
904+
} else {
905+
val hc = computeHash(k)
906+
insertifhc(k, hc, v, INode.KEY_ABSENT) match {
907+
case Some(oldv) => oldv
908+
case None => v
909+
}
910+
}
911+
}
912+
}
913+
876914
def remove(k: K, v: V): Boolean = {
877915
val hc = computeHash(k)
878916
removehc(k, v, hc).nonEmpty

src/library/scala/collection/mutable/MapLike.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ trait MapLike[A, B, +This <: MapLike[A, B, This] with Map[A, B]]
178178
*
179179
* Otherwise, computes value from given expression `op`, stores with key
180180
* in map and returns that value.
181+
*
182+
* Concurrent map implementations may evaluate the expression `op`
183+
* multiple times, or may evaluate `op` without inserting the result.
184+
*
181185
* @param key the key to test
182186
* @param op the computation yielding the value to associate with `key`, if
183187
* `key` is previously unbound.

test/files/scalacheck/Ctrie.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,25 @@ object Test extends Properties("concurrent.TrieMap") {
186186
})
187187
}
188188

189+
property("concurrent getOrElseUpdate") = forAll(threadCounts, sizes) {
190+
(p, sz) =>
191+
val totalInserts = new java.util.concurrent.atomic.AtomicInteger
192+
val ct = new TrieMap[Wrap, String]
193+
194+
val results = inParallel(p) {
195+
idx =>
196+
(0 until sz) foreach {
197+
i =>
198+
val v = ct.getOrElseUpdate(Wrap(i), idx + ":" + i)
199+
if (v == idx + ":" + i) totalInserts.incrementAndGet()
200+
}
201+
}
202+
203+
(totalInserts.get == sz) && ((0 until sz) forall {
204+
case i => ct(Wrap(i)).split(":")(1).toInt == i
205+
})
206+
}
207+
189208
}
190209

191210

0 commit comments

Comments
 (0)