How to Create a Simple In Memory Cache in Java (Lightweight Cache)

High performance scalable web applications often use a distributed in-memory data cache in front of or in place of robust persistent storage for some tasks. In Java Applications it is very common to use in Memory Cache for better performance. But what is “Cache?”

A cache is an area of local memory that holds a copy of frequently accessed data that is otherwise expensive to get or compute. Examples of such data include a result of a query to a database, a disk file or a report.

Lets look at creating and using a simple thread-safe Java in-memory cache.

Here are the characteristic of the In Memory Cache and program CrunchifyInMemoryCache.java

  • Items will expire based on a time to live period.
  • Cache will keep most recently used items if you will try to add more items then max specified. (apache common collections has a LRUMap, which, removes the least used entries from a fixed sized map)
  • For the expiration of items we can timestamp the last access and in a separate thread remove the items when the time to live limit is reached. This is nice for reducing memory pressure for applications that have long idle time in between accessing the cached objects.
  • We will also create test class: CrunchifyInMemoryCacheTest.java

Do you have any of below questions/problems?

  • caching – Lightweight Java Object cache API
  • caching – Looking for simple Java in-memory cache
  • How to create thread-safe in memory caching?
  • Simple Caching for Java Applications
  • Simple Java Caching System
Then this simple Cache implementation is for you.
how-to-create-a-simple-in-memory-cache-in-java

Here is a complete package outline..

Very Simple In Memory Cache Outline

Other must read:

CrunchifyInMemoryCache.java

package crunchify.com.tutorials;

import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.map.LRUMap;

import java.util.ArrayList;

/**
 * @author Crunchify.com
 * How to Create a Simple In Memory Cache in Java (Lightweight Cache)
 *
 */

public class CrunchifyInMemoryCache<K, T> {

        private final long timeToLive;
        
        // LRUMap: A Map implementation with a fixed maximum size which removes the least recently used entry if an entry is added when full.
        // The least recently used algorithm works on the get and put operations only.
        // Iteration of any kind, including setting the value by iteration, does not change the order.
        // Queries such as containsKey and containsValue or access via views also do not change the order.
        private final LRUMap crunchifyCacheMap;

        protected class CrunchifyCacheObject {
                
                // currentTimeMillis(): Returns the current time in milliseconds.
                // Note that while the unit of time of the return value is a millisecond,
                // the granularity of the value depends on the underlying operating system and may be larger.
                // For example, many operating systems measure time in units of tens of milliseconds.
                public long lastAccessed = System.currentTimeMillis();
                public T value;

                protected CrunchifyCacheObject(T value) {
                        this.value = value;
                }
        }

        public CrunchifyInMemoryCache(long crunchifyTimeToLive, final long crunchifyTimerInterval, int maxItems) {
                this.timeToLive = crunchifyTimeToLive * 1000;

                crunchifyCacheMap = new LRUMap(maxItems);

                if (timeToLive > 0 && crunchifyTimerInterval > 0) {

                        Thread t = new Thread(new Runnable() {
                                public void run() {
                                        while (true) {
                                                try {
                                                        
                                                        // Thread: A thread is a thread of execution in a program.
                                                        // The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.
                                                        Thread.sleep(crunchifyTimerInterval * 1000);
                                                } catch (InterruptedException ex) {
                                                        ex.printStackTrace();
                                                }
                                                crunchifyCleanup();
                                        }
                                }
                        });

                        // setDaemon(): Marks this thread as either a daemon thread or a user thread.
                        // The Java Virtual Machine exits when the only threads running are all daemon threads.
                        // This method must be invoked before the thread is started.
                        t.setDaemon(true);
                        
                        // start(): Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.
                        // The result is that two threads are running concurrently:
                        // the current thread (which returns from the call to the start method) and the other thread (which executes its run method).
                        t.start();
                }
        }

        public void put(K key, T value) {
                synchronized (crunchifyCacheMap) {
                        
                        // put(): Puts a key-value mapping into this map.
                        crunchifyCacheMap.put(key, new CrunchifyCacheObject(value));
                }
        }

        public T get(K key) {
                synchronized (crunchifyCacheMap) {
                        CrunchifyCacheObject c;
                        c = (CrunchifyCacheObject) crunchifyCacheMap.get(key);

                        if (c == null)
                                return null;
                        else {
                                c.lastAccessed = System.currentTimeMillis();
                                return c.value;
                        }
                }
        }

        public void remove(K key) {
                synchronized (crunchifyCacheMap) {
                        crunchifyCacheMap.remove(key);
                }
        }

        public int size() {
                synchronized (crunchifyCacheMap) {
                        return crunchifyCacheMap.size();
                }
        }

        public void crunchifyCleanup() {

                // System: The System class contains several useful class fields and methods.
                // It cannot be instantiated. Among the facilities provided by the System class are standard input, standard output,
                // and error output streams; access to externally defined properties and environment variables;
                // a means of loading files and libraries; and a utility method for quickly copying a portion of an array.
                long now = System.currentTimeMillis();
                ArrayList<K> deleteKey = null;

                synchronized (crunchifyCacheMap) {
                        MapIterator itr = crunchifyCacheMap.mapIterator();

                        // ArrayList: Constructs an empty list with the specified initial capacity.
                        // size(): Gets the size of the map.
                        deleteKey = new ArrayList<K>((crunchifyCacheMap.size() / 2) + 1);
                        K key = null;
                        CrunchifyCacheObject c = null;

                        while (itr.hasNext()) {
                                key = (K) itr.next();
                                c = (CrunchifyCacheObject) itr.getValue();

                                if (c != null && (now > (timeToLive + c.lastAccessed))) {
                                        
                                        // add(): Appends the specified element to the end of this list.
                                        deleteKey.add(key);
                                }
                        }
                }

                for (K key : deleteKey) {
                        synchronized (crunchifyCacheMap) {
                                
                                // remove(): Removes the specified mapping from this map.
                                crunchifyCacheMap.remove(key);
                        }

                        // yield(): A hint to the scheduler that the current thread is willing to
                        // yield its current use of a processor.
                        // The scheduler is free to ignore this hint.
                        Thread.yield();
                }
        }
}

CrunchifyInMemoryCacheTest.java

Checkout all comments inside for understanding.

package crunchify.com.tutorials;

/**
 * @author Crunchify.com
 * How to Create a Simple In Memory Cache in Java (Lightweight Cache)
 */

public class CrunchifyInMemoryCacheTest {

        public static void main(String[] args) throws InterruptedException {

                CrunchifyInMemoryCacheTest crunchifyCache = new CrunchifyInMemoryCacheTest();
                
                crunchifyPrint("\n\n==========Test1: crunchifyTestAddRemoveObjects ==========");
                crunchifyCache.crunchifyTestAddRemoveObjects();
                
                crunchifyPrint("\n\n==========Test2: crunchifyTestExpiredCacheObjects ==========");
                crunchifyCache.crunchifyTestExpiredCacheObjects();
                
                crunchifyPrint("\n\n==========Test3: crunchifyTestObjectsCleanupTime ==========");
                crunchifyCache.crunchifyTestObjectsCleanupTime();
        }

        private void crunchifyTestAddRemoveObjects() {

                // Test with crunchifyTimeToLive = 200 seconds
                // crunchifyTimerInterval = 500 seconds
                // maxItems = 6
                CrunchifyInMemoryCache<String, String> cache = new CrunchifyInMemoryCache<String, String>(200, 500, 6);

                cache.put("eBay", "eBay");
                cache.put("Paypal", "Paypal");
                cache.put("Google", "Google");
                cache.put("Microsoft", "Microsoft");
                cache.put("Crunchify", "Crunchify");
                cache.put("Facebook", "Facebook");
                
                crunchifyPrint("6 Cache Object Added.. cache.size(): " + cache.size());
                cache.remove("IBM");
                crunchifyPrint("One object removed.. cache.size(): " + cache.size());

                cache.put("Twitter", "Twitter");
                cache.put("SAP", "SAP");
                crunchifyPrint("Two objects Added but reached maxItems.. cache.size(): " + cache.size());

        }
        
        private static void crunchifyPrint(String s) {
                
                System.out.println(s);
        }
        
        private void crunchifyTestExpiredCacheObjects() throws InterruptedException {

                // Test with crunchifyTimeToLive = 1 second
                // crunchifyTimerInterval = 1 second
                // maxItems = 10
                CrunchifyInMemoryCache<String, String> cache = new CrunchifyInMemoryCache<String, String>(1, 1, 10);

                cache.put("eBay", "eBay");
                cache.put("Paypal", "Paypal");
                
                // Adding 3 seconds sleep. Both above objects will be removed from
                // Cache because of timeToLiveInSeconds value
                Thread.sleep(3000);
                
                crunchifyPrint("Two objects are added but reached timeToLive. cache.size(): " + cache.size());

        }

        private void crunchifyTestObjectsCleanupTime() throws InterruptedException {
                int size = 500000;

                // Test with timeToLiveInSeconds = 100 seconds
                // timerIntervalInSeconds = 100 seconds
                // maxItems = 500000

                CrunchifyInMemoryCache<String, String> cache = new CrunchifyInMemoryCache<String, String>(100, 100, 500000);

                for (int i = 0; i < size; i++) {
                        
                        // toString(): Returns a String object representing the specified integer.
                        // The argument is converted to signed decimal representation and returned as a string,
                        // exactly as if the argument and radix 10 were given as arguments to the toString(int, int) method.
                        String value = Integer.toString(i);
                        cache.put(value, value);
                }

                Thread.sleep(200);

                long start = System.currentTimeMillis();
                cache.crunchifyCleanup();
                double finish = (double) (System.currentTimeMillis() - start) / 1000.0;
                
                crunchifyPrint("Cleanup times for " + size + " objects are " + finish + " s");

        }

}

Just run above program as a Java Application and you will see result as below.

Output:

Here is a IntelliJ IDEA console result.

==========Test1: crunchifyTestAddRemoveObjects ==========
6 Cache Object Added.. cache.size(): 6
One object removed.. cache.size(): 6
Two objects Added but reached maxItems.. cache.size(): 6


==========Test2: crunchifyTestExpiredCacheObjects ==========
Two objects are added but reached timeToLive. cache.size(): 0


==========Test3: crunchifyTestObjectsCleanupTime ==========
Cleanup times for 500000 objects are 0.023 s

Process finished with exit code 0

Cheers…!! Happy Coding.. Let me know if you face any issue running above In Memory Cache Java utility.

The post How to Create a Simple In Memory Cache in Java (Best Lightweight Java Cache) appeared first on Crunchify.