All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fiftyone.caching.LruCacheBase Maven / Gradle / Ivy

There is a newer version: 4.4.63
Show newest version
/* *********************************************************************
 * This Original Work is copyright of 51 Degrees Mobile Experts Limited.
 * Copyright 2023 51 Degrees Mobile Experts Limited, Davidson House,
 * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
 *
 * This Original Work is licensed under the European Union Public Licence
 * (EUPL) v.1.2 and is subject to its terms as set out below.
 *
 * If a copy of the EUPL was not distributed with this file, You can obtain
 * one at https://opensource.org/licenses/EUPL-1.2.
 *
 * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
 * amended by the European Commission) shall be deemed incompatible for
 * the purposes of the Work and the provisions of the compatibility
 * clause in Article 5 of the EUPL shall not apply.
 *
 * If using the Work as, or as part of, a network application, by
 * including the attribution notice(s) required under Article 5 of the EUPL
 * in the end user terms of the application under an appropriate heading,
 * such notice(s) shall fulfill the requirements of that article.
 * ********************************************************************* */

package fiftyone.caching;

import java.io.Closeable;
import java.lang.reflect.Array;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * This is a Least Recently Used (LRU) cache with multiple linked lists
 * in place of the usual single linked list.
 *
 * The linked list to use is assigned at random and stored in the cached
 * item. This will generate an even set of results across the different
 * linked lists. The approach reduces the probability of the same linked
 * list being locked when used in a environments with a high degree of
 * concurrency. If the feature is not required then the constructor should be
 * provided with a concurrency value of 1 so that a single linked list
 * is used.
 * 
 * Although this cache is written to be very generic, the primary use-case
 * is to provide a result cache for Aspect Engines in the Pipeline API.
 * @see Specification
 * 
 * @param  key for the cache items
 * @param  value for the cache items
 */
@SuppressWarnings("unused")
public abstract class LruCacheBase implements Cache, Closeable {

    /**
     * A array of doubly linked lists. Not marked private so that the unit
     * test can guard the elements.
     */
    final CacheLinkedList[] linkedLists;
    /**
     * Random number generator used to select the linked list to use with
     * the new item being added to the cache.
     */
    final Random random = new Random();
    /**
     * Hash map of keys to item values.
     */
    private final ConcurrentHashMap hashMap;
    private final AtomicLong misses = new AtomicLong(0);
    private final AtomicLong requests = new AtomicLong(0);
    private boolean closed = false;
    private final Object lock = new Object();
    private int cacheSize;
    private final boolean updateExisting;

    /**
     * Constructs a new instance of the cache.
     *
     * @param cacheSize the number of items to store in the cache
     * @param concurrency the expected concurrent accesses to the cache
     * @param updateExisting true if existing items should be replaced
     */
    LruCacheBase(int cacheSize, int concurrency, boolean updateExisting) {
        if (concurrency <= 0) {
            throw new IllegalArgumentException(
                "Concurrency must be a positive integer greater than 0.");
        }
        this.cacheSize = cacheSize;
        this.updateExisting = updateExisting;
        this.hashMap = new ConcurrentHashMap<>(cacheSize);
        @SuppressWarnings("unchecked")
        CacheLinkedList[] linkedListsUnchecked =
            (CacheLinkedList[]) Array.newInstance(
                CacheLinkedList.class,
                concurrency);
        linkedLists = linkedListsUnchecked;
        for (int i = 0; i < linkedLists.length; i++) {
            linkedLists[i] = new CacheLinkedList(this);
        }
    }

    /**
     * The number of items the cache lists should have capacity for.
     *
     * @return capacity of the cache.
     */
    public long getCacheSize() {
        return cacheSize;
    }

    /**
     * @return number of cache misses.
     */
    public long getCacheMisses() {
        return misses.get();
    }

    /**
     * @return number of requests received by the cache.
     */
    public long getCacheRequests() {
        return requests.get();
    }

    /**
     * @return the percentage of times cache request did not return a result.
     */
    public double getPercentageMisses() {
        return misses.doubleValue() / requests.doubleValue();
    }

    @Override
    public V get(K key) {
        requests.incrementAndGet();
        // First, try to get the item from the hashMap
        CachedItem node = hashMap.get(key);
        if (node == null) {
            misses.incrementAndGet();
            return null;
        }
        else {
            // The item is in the dictionary.
            // Move the item to the head of it's LRU list.
            node.list.moveFirst(node);
        }

        return node.value;
    }

    protected CachedItem add(K key, V value) {
        // Get a randomly selected linked list to add
        // the item to.
        CachedItem newNode = new CachedItem(
            getRandomLinkedList(),
            key,
            value);

        // If the node has already been added to the dictionary
        // then get it, otherwise add the one just fetched.
        CachedItem node = hashMap.putIfAbsent(key, newNode);

        // If the node was absent and was added to the dictionary (node == null)
        // then it needs to be added to the linked list.
        if (node == null) {
            newNode.list.addNew(newNode);
            node = newNode;
        }
        else {
            if (updateExisting) {
                newNode.list.replace(node, newNode);
                node = newNode;
            }
            // The item is in the dictionary.
            // Move the item to the head of it's LRU list.
            node.list.moveFirst(node);
        }
        return node;
    }

    /**
     * Resets the 'stats' for the cache.
     */
    public void resetCache() {
        this.hashMap.clear();
        misses.set(0);
        requests.set(0);
        for (CacheLinkedList linkedList : linkedLists) {
            linkedList.clear();
        }
    }

/*
    @SuppressWarnings("deprecation")
    @Override
    public void finalize() {
        close(false);
    }
*/

    @Override
    public void close() {
        close(true);
    }

    protected void close(boolean closing) {
        // Clear the map and linked lists.
        if (closed == false) {
            synchronized (lock) {
                if (closed == false) {
                    hashMap.clear();
                    for (CacheLinkedList list : linkedLists) {
                        list.clear();
                    }
                }
                closed = true;
            }
        }
    }

    /**
     * Returns a random linked list.
     */
    private CacheLinkedList getRandomLinkedList() {
        return linkedLists[random.nextInt(linkedLists.length)];
    }

    /**
     * An item stored in the cache along with references to the next and
     * previous items.
     */
    class CachedItem {

        /**
         * Key associated with the cached item.
         */
        final K key;

        /**
         * Value of the cached item.
         */
        final V value;
        /**
         * The linked list the item is part of.
         */
        final CacheLinkedList list;
        /**
         * The next item in the linked list.
         */
        CachedItem next;
        /**
         * The previous item in the linked list.
         */
        CachedItem previous;
        /**
         * Indicates that the item is valid and added to the linked list.
         * It is not in the process of being manipulated by another thread
         * either being added to the list or being removed.
         */
        boolean isValid;

        public CachedItem(CacheLinkedList list, K key, V value) {
            this.list = list;
            this.key = key;
            this.value = value;
        }
    }

    /**
     * A linked list used in the LruCache implementation.
     * This linked list implementation enables items to be moved
     * within the linked list.
     */
    class CacheLinkedList {

        /**
         * The cache that the list is part of.
         */
        LruCacheBase cache = null;

        /**
         * The first item in the list.
         */
        CachedItem first = null;

        /**
         * The last item in the list.
         */
        CachedItem last = null;

        /**
         * Constructs a new instance of the CacheLinkedList.
         */
        public CacheLinkedList(LruCacheBase cache) {
            this.cache = cache;
        }

        /**
         * Adds a new cache item to the linked list.
         */
        void addNew(CachedItem item) {
            boolean added = false;
            if (item != first) {
                synchronized (this) {
                    if (item != first) {
                        if (first == null) {
                            // First item to be added to the queue.
                            first = item;
                            last = item;
                        } else {
                            // Add this item to the head of the linked list.
                            item.next = first;
                            first.previous = item;
                            first = item;

                            // Set flag to indicate an item was added and if
                            // the cache is full an item should be removed.
                            added = true;
                        }

                        // Indicate the item is now ready for another thread
                        // to manipulate and is fully added to the linked list.
                        item.isValid = true;
                    }
                }
            }

            // Check if the linked list needs to be trimmed as the cache
            // size has been exceeded.
            if (added && cache.hashMap.size() > cache.cacheSize) {
                synchronized (this) {
                    if (cache.hashMap.size() > cache.cacheSize) {
                        // Indicate that the last item is being removed from
                        // the linked list.
                        last.isValid = false;

                        // Remove the item from the dictionary before
                        // removing from the linked list.
                        cache.hashMap.remove(last.key);
                        last = last.previous;
                        last.next = null;
                    }
                }
            }
        }

        /**
         * Set the first item in the linked list to the item provided.
         */
        void moveFirst(CachedItem item) {
            if (item != first && item.isValid == true) {
                synchronized (this) {
                    if (item != first && item.isValid == true) {
                        if (item == last) {
                            // The item is the last one in the list so is
                            // easy to remove. A new last will need to be
                            // set.
                            last = item.previous;
                            last.next = null;
                        } else {
                            // The item was not at the end of the list.
                            // Remove it from it's current position ready
                            // to be added to the top of the list.
                            item.previous.next = item.next;
                            item.next.previous = item.previous;
                        }

                        // Add this item to the head of the linked list.
                        item.next = first;
                        item.previous = null;
                        first.previous = item;
                        first = item;
                    }
                }
            }
        }

        /**
         * Replace an existing item in the cache with a new value. The new
         * item must have the same key as the existing item.
         * @param oldItem existing item to replace
         * @param newItem new item to replace it with
         */
        void replace(CachedItem oldItem, CachedItem newItem) {
            if (oldItem.isValid) {
                synchronized (this) {
                    if (oldItem.isValid) {
                        newItem.previous = oldItem.previous;
                        newItem.next = oldItem.next;

                        if (newItem.previous == null) {
                            first = newItem;
                        }
                        else {
                            newItem.previous.next = newItem;
                        }

                        if (newItem.next == null) {
                            last = newItem;
                        }
                        else {
                            newItem.next.previous = newItem;
                        }

                        // Indicate the item is now ready for another thread
                        // to manipulate and is fully added to the linked list.
                        newItem.isValid = true;
                        oldItem.isValid = false;

                        cache.hashMap.replace(newItem.key, newItem);
                    }
                }
            }
        }

        /**
         * Clears all items from the linked list.
         */
        void clear() {
            first = null;
            last = null;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy