HashMap vs ConcurrentHashMap in Java

Compare HashMap and ConcurrentHashMap with code for null keys, iteration and atomic counter updates. Learn when each map fits shared access.

HashMap and ConcurrentHashMap both store values under unique keys. The main difference is how they behave when threads share the map. HashMap does not coordinate concurrent updates, while ConcurrentHashMap supports concurrent reads and updates. Choose based on who changes the map and which operations must happen together.

1. What Do HashMap and ConcurrentHashMap Have in Common?

Both classes implement Map, so they share familiar operations such as put(), get() and remove(). The examples below use the same customer request counts to show what each class provides.

1.1. When Is HashMap Appropriate?

HashMap is a general-purpose map. It is useful when one thread builds and updates the counts. Several threads can also read a map that was fully built before they received it, provided no thread changes it afterward. HashMap does not preserve insertion order.

Map<String, Integer> counts = new HashMap<>();
counts.put("C-17", 1);
counts.put("C-17", 2);

System.out.println(counts.get("C-17")); // 2

The second put() replaces the value for the existing key. This behavior is the same in ConcurrentHashMap. The difference appears when other threads start accessing or changing the same map.

1.2. When Is ConcurrentHashMap Appropriate?

ConcurrentHashMap is a ConcurrentMap implementation for maps that threads read and update together. Its individual operations are thread-safe, and a get() generally does not wait for an unrelated update to finish.

Map<String, Integer> counts = new ConcurrentHashMap<>();
counts.put("C-17", 1);
counts.put("C-17", 2);

System.out.println(counts.get("C-17")); // 2

The result is still 2. Choosing ConcurrentHashMap changes the concurrency guarantees, not the meaning of a normal put() or get(). It does not turn several separate calls into one atomic action.

2. How Do the Maps Differ in Concurrency, Null Handling and Iteration?

The table compares the behavior a caller can rely on. A later example shows why the distinction between one operation and a sequence matters.

BehaviorHashMapConcurrentHashMap
Concurrent updatesRequires external coordinationSupports concurrent updates
Concurrent readsSafe after the completed map is safely shared and no thread changes itCan overlap with updates
null keyAllows oneRejects it
null valuesAllows themRejects them
Iterator while the map changesMay throw ConcurrentModificationException on a best-effort basisIs weakly consistent and does not throw that exception for a concurrent change
A separate get() followed by put()Not atomicStill not atomic
Iteration orderNot guaranteedNot guaranteed

2.1. Which Map Accepts null Keys and Values?

HashMap accepts one null key and multiple null values, while ConcurrentHashMap accepts neither. A second put() with a null key replaces the first value because a map stores only one value per key.

Map<String, Integer> regular = new HashMap<>();
regular.put(null, 1);
regular.put("C-17", null);

System.out.println(regular.get(null)); // 1
System.out.println(regular.containsKey("C-17")); // true

The second lookup would return null, whether C-17 were absent or mapped to null. containsKey() distinguishes those cases for HashMap. ConcurrentHashMap rejects both kinds of null at insertion.

Map<String, Integer> concurrent = new ConcurrentHashMap<>();

try {
    concurrent.put(null, 1);
} catch (NullPointerException ex) {
    System.out.println("null key rejected");
}

try {
    concurrent.put("C-17", null);
} catch (NullPointerException ex) {
    System.out.println("null value rejected");
}

Both messages are printed. Since this map cannot store a null value, a null result from its get() means the key has no mapping at that moment. Check for meaningful nulls before replacing one class with the other.

HashMap does not synchronize an update with another thread’s read or update. A safely shared HashMap can serve concurrent readers when no thread changes it after publication. The ConcurrentHashMap API supports retrievals and updates that overlap. These guarantees apply to individual map operations, not a transaction across several calls.

2.2. How Do Their Iterators Behave During Updates?

An iterator from HashMap may detect a structural change and throw ConcurrentModificationException. Detection is best effort, so the exception is a bug signal rather than a thread-safety mechanism. This code changes the map after obtaining its iterator.

Map<String, Integer> regular = new HashMap<>();
regular.put("A", 1);
Iterator<String> keys = regular.keySet().iterator();
regular.put("B", 2);

try {
    keys.next();
} catch (ConcurrentModificationException ex) {
    System.out.println("HashMap detected a change");
}

The iterator prints the message. The comparable ConcurrentHashMap operation completes without that exception.

ConcurrentHashMap<String, Integer> concurrent = new ConcurrentHashMap<>();
concurrent.put("A", 1);
Iterator<String> concurrentKeys = concurrent.keySet().iterator();
concurrent.put("B", 2);

while (concurrentKeys.hasNext()) {
    concurrentKeys.next();
}
System.out.println("iteration completed");

The second iterator is weakly consistent. It can continue while the map changes and may include some entries added during traversal. It is not a snapshot, and the code must not depend on seeing every concurrent change.

2.3. How Does merge() Prevent a Lost Increment?

Suppose two API requests increment the count for the same customer. Both can read 10 before either stores a new value. The map makes each call thread-safe, but a get() followed by a put() is still two calls.

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.put("C-17", 10);

int firstRead = counts.get("C-17");  // request A reads 10
int secondRead = counts.get("C-17"); // request B reads 10
counts.put("C-17", firstRead + 1);
counts.put("C-17", secondRead + 1);

System.out.println(counts.get("C-17")); // 11, not 12

The example fixes the call order so the lost update can be seen every time. Both requests calculate 11, and the second write replaces 11 with 11. In a real application, overlapping requests can produce that order.

Use merge() to make each increment an atomic update for that key. The next code starts again from 10.

counts.put("C-17", 10);
counts.merge("C-17", 1, Integer::sum);
counts.merge("C-17", 1, Integer::sum);

System.out.println(counts.get("C-17")); // 12

Each merge() sees the current value for C-17 as part of one atomic map operation. The first call stores 11, and the second stores 12. The diagram shows why the separate calls lose a count while the merge() calls keep both increments.

Two requests both read count 10 and write 11, losing one increment. Atomic updates move the count from 10 to 11 to 12.

The atomicity guarantee is for one map call and one key. A calculation involving several keys needs a separate coordination design. A count that can exceed Integer.MAX_VALUE also needs an explicit overflow policy.

3. Which Map Should We Choose for a Shared Counter?

Use HashMap when updates happen in one thread, or when the application builds a map, safely shares it and then only reads it. Safe sharing means other threads receive the completed map before they start using it. Do not change that map while those threads read it.

Use ConcurrentHashMap when several threads must update the same map. Choose an operation such as merge(), compute() or putIfAbsent() when the action depends on the current value. The right method depends on whether the application is incrementing, computing or inserting only when absent.

If one lock must cover several related map actions, a Collections.synchronizedMap() wrapper can be used with an explicit synchronized block. Keep the backing map private. The wrapper keeps the backing map’s null rules and requires synchronization on the wrapper during iteration.

The shared-counter decision follows the operation that must be protected.

Map<String, Integer> oneThread = new HashMap<>();
ConcurrentHashMap<String, Integer> shared = new ConcurrentHashMap<>();

oneThread.merge("C-17", 1, Integer::sum); // caller owns this map
shared.merge("C-17", 1, Integer::sum);    // concurrent callers update one key

The same method name does not give the two maps the same concurrency guarantee. The first map relies on its caller keeping updates in one thread or coordinating them externally. The second map makes each merge() atomic for its key.

Do not choose between these maps from an unqualified speed claim. The result depends on the mix of reads and writes, key distribution and contention. Measure the application’s workload if throughput affects the decision.

For more detail on key lookup, see designing a good HashMap key.

Happy Learning!

Leave a Comment

  1. Thank you very much Lokesh. The explanation indeed answered my query.

    I am now bit more confused on hashcodes of objects and the hashcode of the keys.

    I have read the post of hashmap internal working and am quite clear of it’s working now.
    But at the same time am little bit confused.

    Could you kindly make your time to explain me the following:

    1. How to relate hashcode of objects, hashcode of keys and the array location/bucket concept.
    2. As Entry is a nested class of the Map and the usage of Entry object is again a key, value pair.
    If I have understood properly, Entry has ‘next’ attribute which helps in pointing to next object in the linked list structure.

    But how the key, value pair are internally shown here in the bucket.
    And what exactly does Entry object mean.
    Is it simply a view of mapping of key, value pair as in ‘entry Set view’ or different.

    Sorry that seems to be basic or silly question, but am confused and need a little help here.

  2. Hi Lokesh,

    Can you kindly please brief on this. I had completely drifted away by the explanation of random/fixed hashcode() value for key.

    ————————————————————————————————————————
    If the hashcode of key object changes every time, the exact location of key-value pair will be calculated different, every time. This way, one object stored in HashMap will be lost forever and there will be very minimum possibility to get it back from map.

    For this same reason, key are suggested to be immutable, so that they return a unique and same hashcode each time requested on same key object.
    ————————————————————————————————————————

    When hashcodes for different objects are recommended to be different, how does the above comment justifies the recommendation.

    Kindly explain!

    • Let’s look at below example:

      import java.util.HashMap;
      import java.util.Random;
      
      public class HashCodeTest
      {
         public static void main(String[] args)
         {
            HashMap&lt;Key, String&gt; map = new HashMap&lt;Key, String&gt;();
            
            map.put(new Key(1), &quot;One&quot;);
            
            System.out.println(map.get(new Key(1)));
         }
      }
      
      class Key {
         
         public Key(Integer k){
            this.i = k;
         }
         
         private Integer i;
         
         public Integer getI()
         {
            return i;
         }
      
         public void setI(Integer i)
         {
            this.i = i;
         }
      
         @Override
         public int hashCode()
         {
            return new Random().nextInt();
         }
         
         @Override
         public boolean equals(Object anotherKey)
         {
            return this.i == ((Key)anotherKey).getI();
         }
      }
      
      Output (if you execute above program n times) : null
      

      You are not able to get the value back which you set for key(1). Try something you think logical and get back the value “One” from map. You will not be able to. Why? because hashcode generated for key(1) was random and you cannot produce same hashcode again with this code (if you do not try it million times).

      Now change the hashCode as below:

      @Override
      public int hashCode()
      {
        return this.i * 31;
      }
      

      Now you will be able to get value “One” back everytime, because hashcode for key(1) is fixed to be 1*31. Similarly, hash code for key(2) will be 2*32. All different objects will produce different hashcodes; and same key will have same hashcode everytime. In this way only you can guarantee that you will be able to get value back from hashmap.

      I hope I answered your query. If not then please let me know.

  3. Hi Lokesh,
    Thankyou for your over Hopefully site, Its really amaging and usefull for us, I m new to java and trying learning concept and obviously i found very greatful by your given theme. Thankyou so much.

    Lokesh – Here i m unable to understand by one of your given comment in post, plz clear to me.

    1) How to design a good key for HashMap

    Key’s hash code is used primarily in conjunction to its equals() method, for putting a key in map and then searching it back from map.

    What does conjunction means here.?
    can u plz clear to me?

    Thanks in advance.

    Devesh A

  4. Thanks Lokesh!!! I read your post – “How Hashmap works”. It helped me understand how it finds the right bucket to place/fetch element. I am wondering how does ConcurrentHashmap find the right Segment(Hashmap) to put/get element. Can you please help?

  5. it eems java 1.7 does not ha whie (true) case that cause infinite loop..correct if its wrong

    public V put(K paramK, V paramV)
    {
    if (paramK == null)
    return putForNullKey(paramV);
    int i = hash(paramK.hashCode());
    int j = indexFor(i, this.table.length);
    for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next)
    {
    Object localObject1;
    if ((localEntry.hash == i) && (((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1))))
    {
    Object localObject2 = localEntry.value;
    localEntry.value = paramV;
    localEntry.recordAccess(this);
    return localObject2;
    }
    }
    this.modCount += 1;
    addEntry(i, paramK, paramV, j);
    return null;
    }

  6. Thanks for the nice post. But I have a question.

    In #2 you have mentioned something “When getting data, a volatile read is used”. Could you explain a bit that what you want to describe as Volatile Read.

  7. Hi,
    In question #2 you have mentioned the concurrentHashMap reads the data through “volatile read”. What you actually means by Volatile read.

  8. Lokesh,

    Very useful article. Thanks for providing such a detailed explanation. It would be very helpful if you could elaborate the race condition issue with HashMap in multi threaded environment with example.

  9. Unless and Until some one is doing research or some one is developing new data structures going into the internals of data structures is merely a waste of valuable time. SUN / oracle has provided us these data structures after much research, so just keep use this appropriately,

    Western Interviewers are interested to know only the correct usage and also upto their project. Indians tend to deviate this and spends time un necessarily.

    • Respectfully disagree. I believe that to use something better, one needs to understand how it works. At least, I can expect good APIs only from those who know how things function internally. If you know the rules better, you will play better.

      • Very mature response Lokesh. And Good article.

        I don’t think there is difference between ‘western’ and ‘eastern’ interviewers. You will find all types, everywhere. Knowledge of internals always makes things better.

      • What the ****!! Are you ****ing insane?? You guys ask such crazy questions during interviews and expect to people to shit about it…

        Why did Sun/Oracle ever expose API’s??? There is a reason behind it.

        Any CS graduate/Software Developer will advise you to “RE-USE”. Don’t reinvent the wheel..

        • Hey Mike, You seems to be so angry for no reason. If everybody follow your advice, then there will never be any new improvement in JDK releases and eventually there will never be any performance improvement. Sorry for the hard words, if they offend you. But I am and I like the guys who just go deep inside every concept and develop their own thought process. If CS grads are like what you are saying, thank god I am not one of them.

          • Unfortunately Mike is quite right although his way of expressing himself is not appropriate. I have been in this business for a very long time and it is typical of some culture to prepare themselves with such 1-million dollar questions so that they seem to know everything and be so good and then are just not even more than an average, IN AVERAGE of course. There are good and bad everywhere. There are people who knows software better and there are people who knows better how to have a good interview . I am quite annoyed about this way of doing it and I certainly do not perform any interview with such questions. I rather go for problem solving. It is up to the candidate to come up with the solution whatever he achieved this by digging in the API which is by the way a good way to learn, studying, or his IQ. To me does not matter.
            I would like to add something I forgot to my own comment below: by looking at the internals API you also expose yourself to the implementation changes of the API which might have the exact opposite effect of what you were looking for in first place. An object is free to change its implementation, hopefully in better!, without changing the API. The API should be clear in how it should be used and what performance are achieved. To know a hash has performance close to K-constant, or rehashing impact is more important than other details.
            a RE-USE is an important aspect of software development which should be known and more popular than some details. I mean details are optional, OOP concepts like RE-USE I don’t consider them optional.
            So I don’t understand what you have to be so proud to say that you are not one of the CS grads, in that particular aspect.I think in reality you don’t but you have been aggravated by Mike posting! I hope so at least.
            The performance improvement should be found in the implementation of the object itself , not in the underlying object. I improve my code not the code of all the software APIs I use or directly through it. I must know which are the best performing API and I will choose that one I like the most.
            These sometime maniacal form of looking to details which takes you closer to a hacker than a software developer I think in my personal opinion is coming from software (like Microsoft) which on purpose is not well documented and hiding details (see the billions of fines they got for not releasing information to allow a free competition, fine issues by both the US and even more Europe). That’s why I like open source software, which is correctly a Development Model not a business model as many think.

          • Hey Fabrizio, that’s real big comment and it can be a separate article in itself (seriously). I do agree with more of what you said in between both comments and also about “aggravated” part. Further, I would like to close this discussion being mostly agree with you and a little disagree. Thanks for your views here, I really appreciate it.

      • If the API is properly documented you don’t need to know the internals and this should be always the case. Unfortunately it is not. Otherwise you break the concept of OOD/OOP where the object is opaque and you don’t need to look at it (for me an API is the interface to an object). OOD/OOP is necessary to decompose a bigger problem in a collection of smaller ones ignoring the details of the smaller ones that becomes your building blocks. People can’t hold any given size-problem. Somone is looking at one of the smaller problem ignoring the others and will know the details and in turn will use the API of other underlying objects and the cycle repeats.. Otherwise whatever size is the project, you will have to dig and know everything and you never end. At this point why not to know one more lawyer and how the JVM works and the differences on different OSs. I think knowing the internals is a necessity step (only a step) in the learning process of a person. It is an opportunity to understand what are the issues and how have been solved. At any level of abstractions most likely the problem solving method is the same and you must exercise. For that reason I disagree more with your statement than what I disagree with who you replied to, with all due respect to both of you.

  10. Nice. But Need to Mention this:
    NOT “HashTable”. The proper one is “Hashtable”, small t.
    But your site and content is good.

    • Definitely.. for example: bubble sort is like this:

      public void bubbleSort(int[] array) {
      boolean swapped = true;
      int j = 0;
      int tmp;
      while (swapped) {
      swapped = false;
      j++;
      for (int i = 0; i < array.length - j; i++) { if (array[i] > array[i + 1]) {
      tmp = array[i];
      array[i] = array[i + 1];
      array[i + 1] = tmp;
      swapped = true;
      }
      }
      }
      }

  11. if I override hashCode() method and always return 1000, then what will be the impact on storing and retrieving in HashMap.

    As I know all the map.Entry will be stored at single index with a large linkedlist.

    Please validate and provide your view.

    • Yes you are correct. All the key-value pairs will be stored at single bucket in form of large linked list. Now, whole behavior of map comes down on nature of equals() method.

      1) If you have not overridden equals() method then you will be able to get value objects back from map, for all different key objects stored. Obviously, similar keys will override the value object. Since, default equals() method in Object class compares reference equality, there are rare chances to get any unexpected result from above said. (Almost impossible)

      2) If you have overridden the equals() method, then result of equality from this overridden method will decide that value objects will be replaced or not.

      In any case, the only factor is definite is performance hit. You are likely to get performance of O(n) in stead of O(1).

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.