15

I have a ConcurrentHashMap where I do the following:

sequences = new ConcurrentHashMap<Class<?>, AtomicLong>();

if(!sequences.containsKey(table)) {
    synchronized (sequences) {
        if(!sequences.containsKey(table))
            initializeHashMapKeyValue(table);
    }
}

My question is - is it unnecessary to make the extra

if(!sequences.containsKey(table))

Check inside the synschronized block so other threads wont initialize the same hashmap value?

Maybe the check is necessary and I am doing it wrong? It seems a bit silly what I'm doing, but I think it is necessary.

R. Oosterholt
  • 7,720
  • 2
  • 53
  • 77
Per Stilling
  • 868
  • 2
  • 9
  • 19
  • When you want to achieve ConcurrentHashMap what is the requirement of Synchronized object lock on this instance. – Anil Feb 13 '13 at 10:44

6 Answers6

22

All operations on a ConcurrentHashMap are thread-safe, but thread-safe operations are not composable. You trying to make atomic a pair of operations: checking for something in the map and, in case it's not there, put something there (I assume). So the answer to your questions is yes, you need to check again, and your code looks ok.

João Fernandes
  • 1,101
  • 5
  • 11
  • 2
    The one case where you may skip the extra check is when you don't mind that the `initializeHashMapKeyValue(table)` will be made more then once for the same `table` value. This may be because `initializeHashMapKeyValue` has no side effects and is fairly cheap. – David Soroko Feb 13 '13 at 11:01
18

You should be using the putIfAbsent methods of ConcurrentMap.

ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong> ();

public long addTo(String key, long value) {
  // The final value it became.
  long result = value;
  // Make a new one to put in the map.
  AtomicLong newValue = new AtomicLong(value);
  // Insert my new one or get me the old one.
  AtomicLong oldValue = map.putIfAbsent(key, newValue);
  // Was it already there? Note the deliberate use of '!='.
  if ( oldValue != newValue ) {
    // Update it.
    result = oldValue.addAndGet(value);
  }
  return result;
}

For the functional purists amongst us, the above can be simplified (or perhaps complexified) to:

public long addTo(String key, long value) {
    return map.putIfAbsent(key, new AtomicLong()).addAndGet(value);
}

And in Java 8 we can avoid the unnecessary creation of an AtomicLong:

public long addTo8(String key, long value) {
    return map.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(value);
}
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • 1
    It might make sense to use the OP's pattern if `initializeHashMapKeyValue` takes a non-trivial amount of time or has side effects. – assylias Feb 13 '13 at 11:11
  • @assylias - If the side effects are unavoidable then I think I would agree but there are many methods to avoid/sidestep side effects using factory methods et-al. I would use this pattern wherever possible because it is guaranteed to be free of race conditions whereas the double-checked-lock pattern is famous for having very subtle issues. – OldCurmudgeon Feb 13 '13 at 11:38
  • 1
    The double-checked locking can have issues, but not in this specific example. Note that I'm not saying that your approach is not appropriate - I'm just saying that the OP's approach might be better in some circumstances. – assylias Feb 13 '13 at 11:41
  • Needs to be `if ( oldValue != null ) return oldValue.addAndGet(value); else return value;` or equivalent because `oldValue` will be `null` when it was indeed absent. This confusing api does not return `newValue` which also prevents the nice simplified version. – zapl May 18 '16 at 20:24
4

You can't get exclusive lock with ConcurrentHashMap. In such case you should better use Synchronized HashMap.

There is already an atomic method to put inside ConcurrentHashMap if the object is not already there; putIfAbsent

rai.skumar
  • 10,309
  • 6
  • 39
  • 55
  • 2
    The fact that you can't get a lock on the map does not prevent you from synchronizing the access to the map. – assylias Feb 13 '13 at 10:47
  • agree... but does it add any value. In fact you will be under a wrong impression. – rai.skumar Feb 13 '13 at 10:55
  • 1
    ConcurrentMap will achieve much better concurrency - so if `sequences.containsKey(table)` is generally true, using a concurrent map still makes sense if performance is a concern. – assylias Feb 13 '13 at 11:10
  • This is a valid suggestion. In most cases avoid doing look ups more than necessary. – ring bearer Jun 05 '15 at 06:37
1

I see what you did there ;-) question is do you see it yourself?

First off all you used something called "Double checked locking pattern". Where you have fast path (first contains) which does not need synchronization if case it is satisfied and slow path which must be synchronized because you do complex operation. Your operation consists of checking if something is inside the map and then putting there something / initializing it. So it does not matter that ConcurrentHashMap is thread safe for single operation because you do two simple operations which must be treated as unit so yes this synchronized block is correct and actually it could be synchronized by anything else for example this.

Marek
  • 1,878
  • 14
  • 20
1

In Java 8 you should be able to replace the double checked lock with .computeIfAbsent:

sequences.computeIfAbsent(table, k -> initializeHashMapKeyValue(k));
Jonathan
  • 5,027
  • 39
  • 48
0

Create a file named dictionary.txt with the following contents:

a
as
an
b
bat
ball

Here we have: Count of words starting with "a": 3

Count of words starting with "b": 3

Total word count: 6

Now execute the following program as: java WordCount test_dictionary.txt 10

public class WordCount {
String fileName;

public WordCount(String fileName) {
    this.fileName = fileName;
}

public void process() throws Exception {
    long start = Instant.now().toEpochMilli();

    LongAdder totalWords = new LongAdder();
    //Map<Character, LongAdder> wordCounts = Collections.synchronizedMap(new HashMap<Character, LongAdder>());
    ConcurrentHashMap<Character, LongAdder> wordCounts = new ConcurrentHashMap<Character, LongAdder>();

    Files.readAllLines(Paths.get(fileName))
        .parallelStream()
        .map(line -> line.split("\\s+"))
        .flatMap(Arrays::stream)
        .parallel()
        .map(String::toLowerCase)
        .forEach(word -> {
            totalWords.increment();
            char c = word.charAt(0);
            if (!wordCounts.containsKey(c)) {
                wordCounts.put(c, new LongAdder());
            }
            wordCounts.get(c).increment();
        });
    System.out.println(wordCounts);
    System.out.println("Total word count: " + totalWords);

    long end = Instant.now().toEpochMilli();
    System.out.println(String.format("Completed in %d milliseconds", (end - start)));
}

public static void main(String[] args) throws Exception {
    for (int r = 0; r < Integer.parseInt(args[1]); r++) {
        new WordCount(args[0]).process();
    }
}

}

You would see counts vary as shown below:

{a=2, b=3}

Total word count: 6

Completed in 77 milliseconds

{a=3, b=3}

Total word count: 6

Now comment out ConcurrentHashMap at line 13, uncomment the line above it and run the program again.

You would see deterministic counts.

Kalyan Ghosh
  • 483
  • 11
  • 12