user10776203
user10776203

Reputation: 181

java non blocking cache implementation

(please note i cannot use external libraries for the cache).

---may it can be done using the stream API? ---

i need to implement a cache, it's has 1 key property:

If the cache is asked for a key that it doesn't contain, it should fetch the data using an externally provided function that reads the data from another source (database or similar).

i've started to create a basic skelaton code:

public interface ICache<K,V> {
}

interface IDataSource<K,V> {
   void put(K key, V value);
   V get(K key);
}


public class Cache<K,V> implements ICache<K,V> {
   Map<K,V> cache = new HashMap<>();
   IDataSource<K,V> dataSource;


public Cache(IDataSource<K,V> dataSrc) {
    dataSource = dataSrc;
}

//may it change to a future? how it can be done?
public V getAsync(K key) {   
    if (cache.containsKey(key)) {
        return cache.get(key);
    }
    else {
        //do some async op
    }
}
}

can you advice?

do u think it's need more features?

Upvotes: 0

Views: 1154

Answers (1)

jbx
jbx

Reputation: 22158

In reality what you are writing is a lazy evaluator. You are providing a Supplier for the value, without computing it the first time. The moment someone asks for your value you compute it and return it, memoizing (caching) it for future use.

Have a look at Vavr's Lazy class, it is doing precisely this (but for a single value). You can take some ideas from what that is doing, and also some extra utility methods like checking if it was already computed.

https://github.com/vavr-io/vavr/blob/master/vavr/src/main/java/io/vavr/Lazy.java

Another option is to simply use ConcurrentHashMap. It provides methods to safely (atomically) update values if they are not in the map.

If you want it to be asynchronous you need to introduce some ExecutorService or use CompletableFuture (with your own ExecutorService or the default thread pool that is used by parallel streams etc.). For example:

public class Cache<K,V> implements ICache<K,V> {
   Map<K,V> cache = new ConcurrentHashMap<>();
   IDataSource<K,V> dataSource;

   public Cache(IDataSource<K,V> dataSrc) {
     dataSource = dataSrc;
   }

   // async non-blocking call
   public CompletableFuture<V> getAsync(K key) {   
     return CompletableFuture.supplyAsync(() -> get(key));
   }

   // blocking call
   public V get(K key) {   
     //computeIfAbsent is atomic and threadsafe, in case multiple CompletableFutures try this in parallel
     return cache.computeIfAbsent(key, (k) -> dataSource.get(k));
   }
}

If you also wanted to have an async direct cache and datasource update you could do something like:

  public CompletableFuture<Void> putAsync(K key, V value) {
    return  CompletableFuture.runAsync(() -> {
      synchronized (cache) {
        dataSource.put(key, value);
        cache.put(key, value);
      }
    }
  }

Although honestly I would avoid having 2 entrypoints to update the dataSource (the cache and the dataSource directly). Also it is difficult to make this completely thread safe without having the synchronized (which blocks concurrent cache puts completely from happening in parallel, even if the key is different).

Upvotes: 2

Related Questions