Nicolas Villacorta
Nicolas Villacorta

Reputation: 341

Is there a problem if I read a HashMap from many threads?

I have many services with his own hashmaps, my hashmaps are inmutables and they get initialized at initializers blocks. So all threads will be reading the hashmaps but never writing on them.

My services are Spring's component, so they are "Spring Singletons".

Is there any problem at using HashMap for this? Should I use other class? Why?

Here is an example of what am I talking about:

@Service
public class TrackingServiceOne extends TrackingService {

    
Map<Integer, String> mapCreditos = new HashMap<Integer, String>();
Map<Integer, String> mapDeudas = new HashMap<Integer, String>();
Map<Integer, String> mapEjemplo = new HashMap<Integer, String>();

{

    mapCreditos.put(1, "hi");
    mapCreditos.put(2, "example");
    // And like this I will populate each map.

}



// My methods will read the maps, never write them.
    
}

Upvotes: 3

Views: 1872

Answers (4)

AnatolyG
AnatolyG

Reputation: 1587

Yes, formally speaking, there may be a problem when you read from a HashMap in a thread other than you use to write. See below...

There is a popular dichotomy: data race and race condition (see, for example https://dzone.com/articles/race-condition-vs-data-race). Since HashMap#get() doesn't modify the state of the HashMap, as other answers say, you don't have a race condition on simultaneous reads. But, you have to avoid a data race as well.

Obviously, you have to guarantee that all the writes are finished before your readers start their reads. This means you need to achieve linearizability between writes and reads. On practice/in terms of java code, this will lead to a kind of HB (https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5) before writes and reads. Just a few examples:

  • Save the reference to the map in a final field after the map is filled-in completely (https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5)
  • Thread#start produces HB edge, so, if you manage threads on your own, pass the filled-in map to the reader threads and only after that call their start(). You should do both pass and start from the writer thread
  • Reading threads are awaiting on a CountDownLatch and writing thread calls countDown() after the map is filled-in. The readers start using the map.
  • ForkJoinPool/ExecutorService may be used as well, since if produces required HBs under the hood. Execute/schedule a task to fill-in the map, and only after the task is completed, execute/schedule all the readers.
  • and so on...

So, if you really guarantee (from JMM point of view) the last write is finished before any read is started - congrats, you don't have a data race as well.

The first example seems to be like your case... Even if Spring does some invisible magic under the hood which leads to correct visibility of your map (no one can prove, yeah? :)), I'd prefer the following canonical and explicit, in terms of JMM, safe publication:

@Service
public class TrackingServiceOne extends TrackingService {
        
    private final Map<Integer, String> mapCreditos;
    private final Map<Integer, String> mapDeudas;
    private final Map<Integer, String> mapEjemplo;

    {
        Map<Integer, String> mapCreditosTemp = new HashMap<Integer, String>();

        mapCreditosTemp.put(1, "hi");
        mapCreditosTemp.put(2, "example");

        mapCreditos = mapCreditosTemp; // publishing to the final field produces the freeze action, which guaranty visibility of the field for readers
    }
 ...   
}

The following less explicit form is also OK, since if we have at least one final field defined in the class, its constructor finishes with the freeze action:

@Service
public class TrackingServiceOne extends TrackingService {
        
    private final Map<Integer, String> mapCreditos = new HashMap<Integer, String>();
    ...

    {

        mapCreditos.put(1, "hi");
        mapCreditos.put(2, "example");
        ...
    }
 ...   
}

Look into https://shipilev.net/blog/2014/all-fields-are-final/ for some interesting HotSpot-related details

Upvotes: 2

Basil Bourque
Basil Bourque

Reputation: 339043

Map.copyOf

As the other Answers say, any Map should be thread-safe for read-only access. To be sure the map is used only for reading and not writing, use a map that cannot be modified.

The modern way to make an unmodifiable map is to use Map.copyOf in Java 10 and later.

Populate your HashMap using a temporary object. Then make an unmodifiable copy to be held as a member field on your object.

Map<Integer, String> mapCreditos = Map.copyOf( myTemporaryHashMap ) ;

Upvotes: 2

Arne Burmeister
Arne Burmeister

Reputation: 20604

In principle there is no problem using a HashMap in a concurrent way as long as you really ensure no modifications!

As code evolves over time you should consider wrapping the map using Collections.unmodifiableMap() to make sure nobody can modify later. To do so, initialization by the constructor may be the easiest way.

Also make sure you don’t expose neither the map itself nor any parts like the keySet() or values() to outside of the class.

Upvotes: 4

Burak Serdar
Burak Serdar

Reputation: 51572

HashMap is safe for read-only concurrent access. Make sure the hashmaps are initialized before the threads are started, and as long as no other thread writes to them, they can be used from multiple threads without any race conditions.

Upvotes: 5

Related Questions