Search code examples
javamultithreadingcachingconcurrenthashmapin-memory

How to handle lock in cache (ConcurrentHashMap) using java


I am designing a cache system using concurrenthashmap which is shared among multiple threads. It also has two methods, get and put. I am unable to handle one scenario. The scenario is, if multiple threads want to get data from the cache and the key is not available, then one thread will get data from the database and put it into thecache (ConcurrentHashMap). The other threads will wait until thread-1 sets data into the cache then other threads will read data from the cache. How will I achieve this.

Thanks in advance.


Solution

  • ConcurrentHashMap#computeIfAbsent

    As commented by Wasserman, the ConcurrentHashMap class offers a computeIfAbsent method to do just what you want. The method works atomically to:

    • See if the map has an entry for that key. If so, returns the value for that key.
    • If no entry found, executors your specified lambda function to produce a value. That value is stored as a key-value entry in the map. And, that value is returned.

    All that work happens atomically, meaning that your map operates in a thread-safe manner without you needing to add any further protection.

    To quote the Javadoc:

    If the specified key is not already associated with a value, attempts to compute its value using the given mapping function and enters it into this map unless null. The entire method invocation is performed atomically.

    Example code using a method-reference for your code to retrieve a value from the database:

    map.computeIfAbsent( myKey , key -> repository::fetchValueForKey ) ;
    

    … or use a method call:

    map.computeIfAbsent( myKey , key -> myRepository.fetchValueForKey( key ) ) ;
    

    Example app

    Here is a complete example app.

    We use a map of tracking which day-of-week is assigned to which person’s name, mapping a String to a java.time.DayOfWeek enum object, a Map< String , DayOfWeek >.

    We start with a map of two entries for Alice & Bob. Our goal is to find a third entry for Carol. If not found, add an entry for that key with a value of DayOfWeek.THURSDAY.

    We define a class Repository which we pretend is doing a call to a database to lookup the value assigned to key of Carol.

    Our task to be executed is defined as a Callable that returns a DayOfWeek object. We submit our Callable object several times to an executor service. That service returns Future objects through which we can track success and retrieve our result (which we expect to be DayOfWeek.THURSDAY object).

    To show results, we dump the Map to console, along with the result of each Future.

    package work.basil.demo.threadmark;
    
    import java.time.DayOfWeek;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.*;
    
    public class MapApp
    {
        public static void main ( String[] args )
        {
            MapApp app = new MapApp();
            app.demo();
        }
    
        private void demo ( )
        {
            Map < String, DayOfWeek > inputs =
                    Map.of(
                            "Alice" , DayOfWeek.MONDAY ,
                            "Bob" , DayOfWeek.TUESDAY
                    );
            ConcurrentMap < String, DayOfWeek > map = new ConcurrentHashMap <>( inputs );
            System.out.println( "INFO - Before: map = " + map );
    
            Repository repository = new Repository();
    
            ExecutorService executorService = Executors.newCachedThreadPool();
    
            Callable < DayOfWeek > task = ( ) -> { return map.computeIfAbsent( "Carol" , ( String personNameKey ) -> {return repository.fetchDayOfWeekForPersonName( personNameKey ); } ); };
            List < Callable < DayOfWeek > > tasks = List.of( task , task , task , task , task );
            List < Future < DayOfWeek > > futures = List.of();
            try
            {
                futures = executorService.invokeAll( tasks );
            }
            catch ( InterruptedException e )
            {
                e.printStackTrace();
            }
    
            executorService.shutdown();
            try { executorService.awaitTermination( 10 , TimeUnit.SECONDS ); } catch ( InterruptedException e ) { e.printStackTrace(); }
    
            System.out.println( "INFO - After: map = " + map );
            futures.stream().forEach( dayOfWeekFuture -> {
                try
                {
                    System.out.println( dayOfWeekFuture.get() );
                }
                catch ( InterruptedException e )
                {
                    e.printStackTrace();
                }
                catch ( ExecutionException e )
                {
                    e.printStackTrace();
                }
            } );
        }
    
        class Repository
        {
            public DayOfWeek fetchDayOfWeekForPersonName ( final String personName )
            {
                return DayOfWeek.THURSDAY;
            }
        }
    }
    

    See this code run live at IdeOne.com.

    INFO - Before: map = {Bob=TUESDAY, Alice=MONDAY}
    INFO - After: map = {Bob=TUESDAY, Alice=MONDAY, Carol=THURSDAY}
    THURSDAY
    THURSDAY
    THURSDAY
    THURSDAY
    THURSDAY