<p><pre><code> public V get(Object key) { synchronized (mutex) { ... } }
public V put(K key, V value) { synchronized (mutex) { ... } }
public V remove(Object key) { synchronized (mutex) { ... } }
</code></pre>
> From a correctness standpoint, this strategy is almost trivial to reason about.<p>It is indeed trivial to reason about. This design invites users to stumble into the race condition between get and set.
What a good transactional API is like is an interesting question.<p><pre><code> public void modify(K key, Function<Optional<V>,Optional<V>> modifier)
</code></pre>
or something along those lines covers the race of a single value being changed out from under you, but if you want to update one or more keys you might want<p><pre><code> public void modifyMany(Set<K> key, Consumer<Map<K,V>> modifier)
</code></pre>
where the modifier gets passed a modifiable view that has just the keys in the set.<p>There is also the Clojure-style immutable API for maps though I can say I never really enjoyed working with that the way I enjoyed immutable lists (which are Lispier than Lisp: I went through all the stages of grief reading <i>On Lisp</i> and couldn't help think "If he was using Clojure he wouldn't be struggling with <i>nconc</i>)
STM containers does it right.<p><pre><code> lookup :: k -> Map K v -> STM (Maybe v)
insert :: v -> k -> Map K v -> STM ()
</code></pre>
It won't execute until you wrap it with atomically, so you're forced to be explicit about whether these things should happen in one logical step or separately.<p><a href="https://hackage-content.haskell.org/package/stm-containers/docs/StmContainers-Map.html" rel="nofollow">https://hackage-content.haskell.org/package/stm-containers/d...</a>
There's a design in the .NET ConcurrentDictionary that is even more inviting:<p><pre><code> GetOrAdd(TKey, Func<Tkey,TValue> )
</code></pre>
This lets you specify a key, and a method to run to generate a value to store if the key does not exist.<p>This is thread-safe, however it is <i>not atomic</i> much to the surprise of many that use it.<p>If you wrongly assume that the factory method can only execute once, you fall into a trap, since it actually can execute many times before the result is stored.<p>This can cause a problem if you use it for caching, because you might for example put an expensive calculation you want to cache there, only to find out cache expiration causes a stampede as the factory suddenly executes a lot at once.
I don't see the problem at all, tbh. Both `get(K); put(K);` and `put(K); get(K);` are valid execution traces on a uniprocessor.