Should I use a thread-safe map implementation with DateTimeFormatter?

62 views Asked by At

Please help me figure it out, I think I'm confused. I have a class contains HashMap<String, DateTimeFormatter> that is populated when the application starts.

public class DateService{
   private final Map<String, DateTimeFormatter> map;
   public PeopleService(String pattern){
     map = new HashMap<String, DateTimeFormatter>();
     map.put(pattern, DateTimeFormatter.ofPattern(pattern)); //it is only test example
   }
}

Should I make it ConcurrentHashMap or volatile to convert the date into multiple threads?

A similar question if I want to use List<DateTimeFormatter> to convert dates in multiple threads

1

There are 1 answers

0
Basil Bourque On

tl;dr

If your intent is a single instance, pre-populated, and unmodified, then the following code should be thread-safe.

If final and unmodifiable, you can provide direct access to the map, if that is helpful in your code situation.

public class DateService
{
    // Member fields.
    public final Map< String, DateTimeFormatter > formatsByPurpose;
    
    // Constructor
    public DateService( … )
    {
        this.formatsByPurpose = 
                Map.of(
                        "gui" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "print" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "json" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "html" , DateTimeFormatter.of( … ).withLocale( … ) ,
                );
        …
    }
}

If you intend to have only a single map created, but may modify its contents, then perhaps this code.

We use Map.of here for its convenient literals-syntax, but pass it to a constructor for another map.

Notice that we define the member field as ConcurrentMap to advertise to the reader its thread-safety, and to double-check our code to ensure we assign only an object that implements ConcurrentMap.

public class DateService
{
    // Member fields.
    public final ConcurrentMap< String, DateTimeFormatter > formatsByPurpose;
    
    // Constructor
    public DateService( … )
    {
        Map< String , DateTimeFormatter > map = 
                Map.of(
                        "gui" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "print" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "json" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "html" , DateTimeFormatter.of( … ).withLocale( … ) ,
                );
        this.formatsByPurpose = new ConcurrentHashMap<>( map ) ;
        …
    }
}

If you intend to possibly replace the entire map with another map object, then use AtomicReference (or, alternatively, mark it volatile).

Notice the …Ref added to the member field name to remind the reader of it being an atomic reference, meaning it takes us one step away from the payload we actually care about (the map).

We mark the atomic reference as final to prevent our member field from ever being assigned to any other AtomicReference object. So the payload (the map object’s reference) may be changed out, but its container (the AtomicReference object) remains fixed.

public class DateService
{
    // Member fields.
    public final AtomicReference< ConcurrentMap< String, DateTimeFormatter > > formatsByPurposeRef = new AtomicReference<>() ;
    
    // Constructor
    public DateService( … )
    {
        ConcurrentMap< String , DateTimeFormatter > map = 
                Map.of(
                        "gui" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "print" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "json" , DateTimeFormatter.of( … ).withLocale( … ) ,
                        "html" , DateTimeFormatter.of( … ).withLocale( … ) ,
                );
        this.formatsByPurposeRef.set( new ConcurrentHashMap<>( map ) ) ;
        …
    }
}

To modify the map stored away inside an atomic reference, go through the atomic.

myDateService.formatsByPurposeRef.get().remove( "html" ) ;  // Optionally capture the returned (and now removed) `DateTimeFormatter` object.

Caveat: All that code is untested, off the top of my head.

Details

Regarding thread-safety, two considerations:

  • DateTimeFormatter objects themselves are guaranteed by the the Javadoc to be thread-safe. Being immutable contributes towards being thread-safe. To quote the Javadoc: Implementation Requirements: This class is immutable and thread-safe.
  • Any Map implementation that you (a) populate ahead of usage, and (b) do not modify (no puts, no removes), is thread-safe for retrieval.

So key part here, which you did not explain, is your intention to (a) modify or not modify the map, and (b) replace or not replace the map.

  • If you might possibly modify the map, be sure to use a thread-safe implementation of Map. The HashMap implementation is not thread-safe under possibly modified. Use a Map class that implements ConcurrentMap. Java comes bundled with two such implementations: ConcurrentHashMap & ConcurrentSkipListMap. You may find third-party ones, perhaps in Eclipse Collections and/or Google Guava.
  • If you intend to not modify the map, use an implementation that is unmodifiable. Then, no thread-safety worries.

Should I make it ConcurrentHashMap

Only if you intend to modify the elements of the map.

Should I make it … volatile

Not if you instantiate (once, and only once) and populate before any possible usage. I personally prefer this approach. Instantiating & populating in a constructor is one good way to do this.

If your intention is to instantiate a single Map object, and keep it unmodifiable, then two tips:

  • Mark the Map field as final to ensure that another map object’s reference is never assigned to that variable.
  • Use Map.of or Map.copyOf to ensure your map cannot be modified.

If you intend to switch out one map object for another during runtime, then you definitely have an "visibility" issue. In that case, you can either (a) mark the field volatile or (b) use an AtomicReference variable to hold the current map object’s reference. I personally prefer the latter because I like how Atomic… screams out to the reader that we are protecting a resource under concurrent access. Having to use the Atomic… methods everywhere keeps us aware of being in a multi-threaded situation.