How to avoid reads before initialization of all the three maps are done using RentrantLock and return updated set of maps after update is done?

StackOverflow https://stackoverflow.com/questions/23369069

Question

I am trying to implement lock by which I want to avoid reads from happening whenever I am doing a write on my three maps. So my requirement is -

  1. Reads block until all three maps have been set for the first time.
  2. Now second time, If I am updating the maps, I can still return all the three old maps value(before the updates are done on all three maps) or it should block and return me all the new three maps value whenever the updates are done on all the three maps.

As I have three Maps - primaryMapping , secondaryMapping and tertiaryMapping so it should return either all the new values of three updated maps or it should return all the old values of the map. Basically, while updating I don't want to return primaryMapping having old values, secondaryMapping having having new values, and tertiaryMapping with new values.

It should be consistent, either it should return old values or it should return new values after updating the maps. In my case, updating of maps will happen once in three months or four months.

Below is my ClientData class in which I am using ReentrantLock in which the whole logic is there -

public class ClientData {

    private static final class MapContainer {
        private Map<String, Map<Integer, String>> value = null;

        public Map<String, Map<Integer, String>> getValue() {
            return value;
        }

        public void setValue(Map<String, Map<Integer, String>> value) {
            this.value = value;
        }
    }

    private static final MapContainer primaryMapping = new MapContainer();
    private static final MapContainer secondaryMapping = new MapContainer();
    private static final MapContainer tertiaryMapping = new MapContainer();
    private static final MapContainer[] containers = {primaryMapping, secondaryMapping, tertiaryMapping};
    private static boolean allset = false;
    private static final Lock lock = new ReentrantLock();
    private static final Condition allsetnow = lock.newCondition();

    private static final Map<String, Map<Integer, String>> getMapping(MapContainer container) {
        lock.lock();
        try {
            while (!allset) {
                allsetnow.await();
            }
            return container.getValue();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // reset interruptedd state.
            throw new IllegalStateException(e);
        } finally {
            lock.unlock();
        }

    }

    public static void setAllMappings(Map<String, Map<Integer, String>> primary,
            Map<String, Map<Integer, String>> secondary,
            Map<String, Map<Integer, String>> tertiary) {
        lock.lock();
        try{

            // how  to avoid this?
            if (allset) {
                throw new IllegalStateException("All the maps are already set");
            }

            primaryMapping.setValue(primary);
            secondaryMapping.setValue(secondary);
            tertiaryMapping.setValue(tertiary);
            allset = true;
            allsetnow.signalAll();
        } finally {
            lock.unlock();
        }
    }       


    public static Map<String, Map<Integer, String>> getPrimaryMapping() {
        return getMapping(primaryMapping);
    }

    public static Map<String, Map<Integer, String>> getSecondaryMapping() {
        return getMapping(secondaryMapping);
    }

    public static Map<String, Map<Integer, String>> getTertiaryMapping() {
        return getMapping(tertiaryMapping);
    }       
}

And below is my background thread code which will get the data from my service URL and it keeps on running every 10 minutes once my application has started up, and then it will parse the data coming from the url and store it in a ClientData class variable in those three maps.

public class TempScheduler {

    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        public void startScheduler() {
            final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate(new Runnable() {
                public void run() {
                try {
                    callServers();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                }
            }, 0, 10, TimeUnit.MINUTES);
        }
    }

    // call the servers and get the data and then parse 
    // the response.
    private void callServers() {
        String url = "url";
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject(url, String.class);
        parseResponse(response);

    }

    // parse the response and store it in a variable
    private void parseResponse(String response) {
        //...       
        ConcurrentHashMap<String, Map<Integer, String>> primaryTables = null;
        ConcurrentHashMap<String, Map<Integer, String>> secondaryTables = null;
        ConcurrentHashMap<String, Map<Integer, String>> tertiaryTables = null;

        //...

        // store the data in ClientData class variables if anything has changed  
        // which can be used by other threads
        if(changed) {
            ClientData.setAllMappings(primaryTables, secondaryTables, tertiaryTables);
        }
    }
}

I will be using getPrimaryMapping, getSecondaryMapping and getTertiaryMapping in ClientData class from the main thread in my main appliction so I want to return either all the new set of values from these three maps or if update is happening, then either block it and return all the new set of values for those three maps after the updates are done.

Problem Statement:-

In my code base as shown above in ClientData class, I guess, I won't be able to update the maps once it has been set for the first time as this line will cause a problem and it will throw a exception and then also how to implement my second point as shown above?

// how  to avoid this?
if (allset) {
    throw new IllegalStateException("All the maps are already set");
}

How can I successfully implement all my above two points? I guess, there is something very minor thing which I am missing here? I want to use ReentrantLock here but any other suggestions are also welcome. My main concern is the performance issues. Because I will be doing set on those three maps once in a three month so this is fine. But get on three maps will be happening from main application code every 1000 requests per second so I want to be pretty fast.

Initially, I was thinking to remove this if statement -

// how  to avoid this?
if (allset) {
    throw new IllegalStateException("All the maps are already set");
}

But I am doubting, it will not work since then there will be mismatched of maps between the threads while I am updating the maps?

And this is the way, I am reading the values from ClientData class from the main application thread -

String data1 = ClientData.getPrimaryMapping().get(some_value1).get(some_value2);
String data2 = ClientData.getSecondaryMapping().get(some_value1).get(some_value3);
String data3 = ClientData.getTertiaryMapping().get(some_value1).get(some_value4);

Update:-

Another solution using CountDownLatch which fulfills all the above conditions -

Below is my ClientData class in which I am using CountDownLatch -

public class ClientData {

    public static class Mappings {
        public final Map<String, Map<Integer, String>> primary;
        public final Map<String, Map<Integer, String>> secondary;
        public final Map<String, Map<Integer, String>> tertiary;

        public Mappings(
            Map<String, Map<Integer, String>> primary,
            Map<String, Map<Integer, String>> secondary,
            Map<String, Map<Integer, String>> tertiary
        ) {
            this.primary = primary;
            this.secondary = secondary;
            this.tertiary = tertiary;
        }
    }

    private static final AtomicReference<Mappings> mappings = new AtomicReference<>();
    private static final CountDownLatch hasBeenInitialized = new CountDownLatch(1);

    public static Mappings getMappings() {
        try {
            hasBeenInitialized.await();
            return mappings.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    }

    public static void setMappings(
        Map<String, Map<Integer, String>> primary,
        Map<String, Map<Integer, String>> secondary,
        Map<String, Map<Integer, String>> tertiary
    ) {
        setMappings(new Mappings(primary, secondary, tertiary));
    }

    public static void setMappings(Mappings newMappings) {
        mappings.set(newMappings);
        hasBeenInitialized.countDown();
    }
}

I will be using ClientData class in main application thread like this -

Mappings mappings = ClientData.getMappings(); 
// use mappings.primary 
// use mappings.secondary 
// use mappings.tertiary

Does this code has any performance impact? In short which one will be better, should I go with ReentrantReadWriteLock or the above CountDownLatch one solution?

So now the questions is, CountDownLatch solution vs ReentrantReadWriteLock solution? Which one will perform better in high read / low write use case?

Can anyone also provide an example which uses ReentrantReadWriteLock basis on my above solution if possible? By that, I will be able to compare performance against CountDownLatch solution which I have earlier vs ReentrantReadWriteLock one.

As I am not able to come up with a solution which uses ReentrantReadWriteLock yet for my above use case.

NOTE:-

In my case, writes will be happening once in three or four month. But reads are going to happen from multiple threads at a very high speed, 1000 requests per second. So it has to be pretty fast.

Was it helpful?

Solution

You should continue your restructuring along the line you're going with the introduction of Mappings. You don't need to manage the three references individually (this is where it gets complex). Instead, manage one reference.

class ClientData {
  // This is immutable after creation (like your OP)
  class Mappings { /* definition from your original post */ }

  // this should be volatile;
  private static volatile Mappings instance;

  // the read path proceeds without acquiring any lock at all.  Hard to 
  // get faster than a volatile read.  Note the double-checked locking pattern
  // works JDK 6 or greater when using volatile references (above)
  public static Mappings getMappings() {
    Mappings result = instance;
    if(result == null) {
       synchronized(ClientData.class) {
          result = instance;
          // recall while() is required to handle spurious wakeup
          while(result == null) {
             ClientData.class.wait();
             result = instance;
          }
       }
    }
  }

  public static setMappings(Map one, Map two, Map three) {
    synchronized(ClientData.class) {
      instance = new Mappings(one,two,three);
      ClientData.class.notifyAll()
    }
  }
}

I think this has the following benefits:

  1. No locks are required on the read path. This is a side effect of the immutability of the Mappings class. Volatile read is really fast.
  2. Callers that enter getMappings() before it simply wait for it to be set.
  3. No third-party objects semantics to worry about

It's somewhat unfortunate (IMHO) that java doesn't have a good "Waitable Reference" built in. But one can't ask for everything! Third party libraries have some support -- Guava's Suppliers.memoize() is a good place to start.

Good luck with your project.

OTHER TIPS

You are right that you need to remove the if (allset) block. Otherwise you won't be able to update.

So, it looks like you have to solve the following problem that could occur:

  1. You call ClientData.getPrimaryMapping()
  2. A wild update appears
  3. You call ClientData.getSecondaryMapping() - now you have mismatching data

Instead of calling all three mapping methods after each other with the possibility of updates in between, I would create a single method to get all maps at once. The method should return a list (or an array or some other kind of container) of your maps. By moving the lock to this method, you can assure that you there won't be any updates while you get your maps.

Something like this for example:

// create a new list on each update
private static List<Map<String, Map<Integer, String>>> mappings;

public static final List<Map<String, Map<Integer, String>>> getAllMappings() {
    lock.lock();
    try {
        while (!allset) {
            allsetnow.await();
        }
        return mappings;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // reset interruptedd state.
        throw new IllegalStateException(e);
    } finally {
        lock.unlock();
    }

}

I guess it could also be faster because of less method calls and less try catch blocks but if you are really concerned about performance you should measure what performs best.

If you have a code block like this

String data1=ClientData.getPrimaryMapping().get(some_value1).get(some_value2);
String data2=ClientData.getSecondaryMapping().get(some_value1).get(some_value3);
String data3=ClientData.getTertiaryMapping().get(some_value1).get(some_value4);

there is nothing you can do within the getMapping method to fix the possible race. There can always be an update between these three lines yielding to inconsistent values for data1, data2, and data3 regardless of how well you sync within the called methods (once you allow updates).

The only way to fix this is to move the sync logic into the caller. In the example code above, all three statements must be placed within the same guarded block. The good news is that this will help your performance as, if you do it properly, you don’t need a lock within the called methods then. So instead of locking and unlocking three times you are doing it only once for the entire block.

For your use case of having tons of reads and only occasional updates, using a ReadWriteLock allowing concurrent reads is a natural choice.

Sounds like a Job for a ReadWriteLock.

Any method or process that will cause the three maps to become inconsistent with one another should lock the write lock before it starts, and unlock the write lock after it is finished:

java.util.concurrent.locks.ReaderWriterLock readerWriterLock = ...;

readerWriterLock.writeLock().lock()
try {
    ...update maps...
} finally {
    readerWriterLock.writeLock().unlock()
}

It will be a little more complicated if more than one thread is involved, but the principle remains the same: Keep the write lock locked when you don't want readers to access the maps.

Any method that needs to use the data in the maps should lock the read lock. That looks just the same as in my example above, but substitute "readLock()" for "writeLock()".

Reads block until all three maps have been set for the first time.

Easy! Just make sure that the writeLock is locked before you start any thread that can read the maps, and make sure to unlock it after the maps have been set to their initial consistent state.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top