Skip to content

ConcurrentLinkedCache implementation

n-lagomarsini edited this page May 30, 2013 · 2 revisions

This new implementation is used because the other cache(ConcurrentTileCache) created behaves quite slower than the default Sun tile cache.

In this work will be used a ConcurrentLinkedHashMap that gives the possibility to check the current memory usage every time is needed for providing a continue memory control on the cache. The availability of the current memory usage is fundamental in a limited size cache with entries having various dimensions like the JAI tiles. For avoiding that the JVM throws an OutOfMemoryError, a correct eviction policy must be set. The default ConcurrentLinkedHashMap eviction policy is based on a LRU algorithm for choosing which element to remove and so having more space. This policy is fulfilled using several buffered threads that execute their work when a new write operation is requested or when the buffer-size is reached.

This new Cache must be thread-safe for being used, so every changes to the cache-state must be atomic. The cache state is composed by some fields:

  • the Cache itself, which contains the tile to be requested.
  • the memory capacity of the cache, that should be initialized before using the cache, but can be changed during the runtime. (default = 16MB)
  • the memory threshold of the cache , that indicates the amount of memory to use. (default = 0.75F)
  • the diagnostic mode:enabled or disabled. (default=disabled)
  • the concurrency level of the map. (default=16)
  • the current memory usage of the cache.
  • the tile count, that indicates the total number of tile present in the cache.
  • the number of cache hits.
  • the number of cache miss.
  • the tile timestamp and action, used for notify tile changes to the observer.

The Cache field has an internal thread-safety provided by the ConcurrentHashMap inside the ConcurrentLinkedHashMap, which is already thread-safe. This map can be accessed by multiple readers simultaneously and by a limited number of writers for improve scalability. Of course it is not enough, because the thread safety should be global. For this purpose some other improvements are made.

The memory capacity, the memory threshold, the diagnostic mode and the concurrency level are set at the cache initialization. If the user needs to change one of them can do it in a thread-safe manner because the related "Set" method are surrounded with a "synchronized" block. When this operation happens, the cache is flushed and re-built. It is worth to point out that the maximum memory capacity is never totally used because the cache starts to evict entries when riches the value maximum memory capacity * memory threshold until the memory currently used came back to that value.

The memory usage during the cache lifecycle is encapsulated inside the ConcurrentLinkedHashMap, so this variable must not be updated during every write operation. For retrieving this information the user should only call the weightedSize() method.

Also the tileCount is incapsulated inside the Map, becoming thread-safe. If the user needs this field can simply call the size() method. This two fields are only approximated because their real values could change during writing operation executed by other thread when the weightedSize() or size() operations are performed.

The hit and miss count are kept in memory only for diagnostic purpose, so that the cache, during normal runtime with diagnostic disabled, doesn't slow his execution. When the diagnostics is enabled this values must be updated during every read or write operation. This update operation must be done in a thread-safe way; the solution to this inconvenient is to encapsulate all the read(or write) operation in a "synchronized" block,so that the entire writing or reading operation is done atomically.

The cached tile status has almost all his fields immutables, instantiated at the creation time, except the "timestamp" and the "action" ones. The first field is set when the tile is created and is updated every time any thread carries out an operation on the same tile. The "action" field is updated at every thread operation on the tile. This update operation are present only if the diagnostics is enabled and are inside the above descripted "synchronized" block for mantaining the atomicity of the operation.

As the user can see, the diagnostic mode defeats the scalbility provided by the ConcurrentLinkedHashMap. This drawback must be accepted for avoiding race-conditions or other inconvenient due to the lack of thread-safety.

This cache implementation has various methods imported by the TileCache and the CacheDiagnostics interface:

  • Add tile/s;
  • Get tile/s;
  • Remove tile/s;
  • Enable/disable diagnostics;
  • Set/Get memory capacity;
  • Set/Get memory threshold;
  • Set/Get concurrency level;
  • Get Hit/MissCount (only in diagnostic mode);
  • Get memoryUsed;
  • Get tile count;
  • ResetCounts (only in diagnostic mode);
  • Flush.

This methods are not supported because not useful to this cache:

  • Set/Get tile capacity (deprecated);
  • Set/Get comparator (ordering inside the ConcurrentLinkedHashMap)
  • MemoryControl (internal eviction policy of ConcurrentLinkedHashMap).

The standard cache methods like add(), get() and remove() are done, in the non-diagnostic mode, simply calling the respective method inside the Map(put, get and remove). Otherwise, in the diagnostic mode, the observer notification methods( setChanged() , notifyObservers()), the updates on the tile status and on the hit/miss count are added for recording the whole cache status.

The flush() method clean the cache eliminating all the tile present and rebuild a new empty cache. In the non-diagnostic mode, the cleaning operation results only in a call to the ConcurrentLinkedHashMap method clear() . In the diagnostic mode instead, every singular tile is updated and then removed. After the tiles removal a new cache is rebuilt.

ResetCount() has a real utility only when the diagnostic mode is enabled, and simply set to 0 the hit and miss count. In non diagnostic mode it does nothing. This write operation is done in a "synchronized" block since hit and miss count are part of the cache status. The reading( get()) of this 2 variable instead is not surrounded by a "synchronized" block beacuse multiple reader can access them concurrently without modifying their status.

As said above, the tile internal status is updated only in diagnostics in a "synchronized" block. The change notification is send to the cache observers with the 2 methods setChanged() and notifyObservers(). The first indicates that some changes has been made to the cache, while the second notifies it to the observers.

In conclusion we can say that this TileCache implementation is similar to the precedent but used a different cache object that provide a different behaviour and eviction policy. From the test it is possible to check which of the two implementation is more scalable.