How to conditionally put a value into concurrent hashmap?

215 Views Asked by At

I maintain a map of high scores for different games. When I receive a new score, I want to be able to check if the score is higher than the current high score, and if so, make it the new high score for that game in the map.

ConcurrentHashMap looks like the way to go, but I'm surprised to see that there doesn't seem to be a simple way to atomically check the current value in the map and update it conditionally.

I have tried using compute, but it seems to me there is no way to say "As a result of my computation I'd like to leave the current entry in place." I was hoping that returning null from the compute function would achieve this, but it removes the current entry.

My example code:

import java.util.concurrent.ConcurrentHashMap;

public class ConcHashMapTest{

    public static void main(String[] args){
        ConcurrentHashMap<String,String> h = new ConcurrentHashMap<>();
        
        h.put("TEST","value1");
        
        /*
         The question is, can I do an atomic CHECK and update ONLY if
         I want to?
         
         I know I can use compute, but do you HAVE to change the map
         or can you return null and leave the mapping as it is?
         */
        
        h.compute("TEST",(k,v) -> {
            if (v.equals("changeme")){
                return "CHANGED";
            }
            else {
                return null;
            }
        });
        
        System.out.println("Now hashmap is "+h);
    }
}

This returns:

Now hashmap is {}

I could of course just return the same value as I found there, but presumably that would cause a lot of updating of that value? Perhaps that's the way to go, though? I don't think so, though, because one of the points of the concurrenthashmap is to allow overlapping reads for speed, and syncronize updates, and to do this it creates a happens before only if there is an update. So I don't want to be updating every time I check the value surely?

Thank you!

EDIT: Oh wait, I think I'm getting confused. The benefit to the concurrent hashmap is that when I'm only READING the high scores (which I do a lot), I can have overlapping reads. And if I want atomicity of updates, my calls HAVE to be syncronised somewhere so that they're sequential?

So I think that using compute and updating the value with itself if I want to leave it unchanged must be the right thing to do? I believe the syncronising only locks that that one mapping (or at least bucket), not the whole map..

EDIT2: Thinking further about it, I'm tempted not to try for atomicity. Although I will be getting potentially thousands of possible highscores for a game every second, genuine high scores will be pretty rare. The chance of two high scores arriving at exactly the same moment such that there is an interleaving problem with:

if (score > map.get("SCORE")){
  map.put("SCORE",score);
}

is so close to zero as to make no difference. I think I'll just leave a small (almost theoretical) hole where I could fail to record a high score.

3

There are 3 best solutions below

2
Reilas On

'... ConcurrentHashMap looks like the way to go, but I'm surprised to see that there doesn't seem to be a simple way to atomically check the current value in the map and update it conditionally. ...'

The ConcurrentHashMap class is optimized for threaded operations, so the provided methods should all function in an atomic manner.

'... I have tried using compute, but it seems to me there is no way to say "As a result of my computation I'd like to leave the current entry in place." I was hoping that returning null from the compute function would achieve this, but it removes the current entry. ...'

Utilize the computeIfPresent method.

Here is an example.

int highScore = 200;
map.computeIfPresent("abc", (k, v) -> highScore > v ? highScore : v);

'... I believe the syncronising only locks that that one mapping (or at least bucket), not the whole map.. ...'

Here is the OpenJDK source for the computeIfPresent method.

At least for this method, the synchronization is on the f Node object, rather than the method, or the instance.
The entire class is quite optimized.

0
DuncG On

A ConcurrentHashMap will ensure thread-safe update for your situation. As well as compute, you could also use merge. merge accepts the new value as a parameter rather than need to encode it in the remapping function. Example:

ConcurrentHashMap<String,Integer> scores = new ConcurrentHashMap<>();
BiFunction<Integer, Integer, Integer> max = 
           (oldv,newv) -> oldv == null || oldv < newv ? newv : oldv;

scores.merge("a", 2, max);
scores.merge("a", 4, max);
scores.merge("b", 9, max);
scores.merge("b", 5, max);

System.out.println("High Scores: "+scores);
// Prints:  High Scores: {a=4, b=9}

As with compute, you need to return the old / existing value to ensure the key is not removed by the operation.

0
marcinj On

You can use the compute method of ConcurrentHashMap to atomically check and conditionally update a value. If you want to leave the current entry in place, you should return the current value instead of null. Returning null from the compute function will remove the current entry. Here is the corrected code:

h.compute("TEST",(k,v) -> {
    if (v != null && v.equals("changeme")){
        return "CHANGED";
    }
    else {
        return v; // return current value to leave the entry in place
    }
});

This way, the value will only be updated if it equals "changeme", otherwise it will remain the same. The compute method ensures atomicity of the operation.

If you are concerned that returning value will update it anyway, then there is no need - it does not cause any problems related to efficiency or thread safety. The whole operation is anyway under synchronized block as you can see here:

https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java#L1942