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

org.osgl.util.LFUCache Maven / Gradle / Ivy

The newest version!
package org.osgl.util;

/*-
 * #%L
 * Java Tool
 * %%
 * Copyright (C) 2014 - 2020 OSGL (Open Source General Library)
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.util.*;

/**
 * A simple thread-safe LFU cache.
 *
 * Disclaim: the source code is adapted from https://github.com/Tsien/LFUCache/
 *
 * @param 
 * @param 
 */
public class LFUCache {

    private class Node {
        private V v;
        private int count;

        Node(V v) {
            this.v = v;
            this.count = 0;
        }

        int touch() {
            return ++this.count;
        }
    }

    // a hash map holding > nodes
    private final Map store;

    // a list of LinkedHashSet, accessCountList[i] has elements with accessCount = i
    private final LinkedHashSet[] accessCountList;

    // the minimum frequency in the cache
    private int minFreq;

    // the size of the cache; it is also the upper bound of possible frequency
    private final int capacity;

    // the number of evicted elements when reaching capacity
    private final int evictNum;

    /**
     * Create a new LFU cache.
     *
     * @param cap         the size of the cache
     * @param evictFactor the percentage of elements for replacement
     * @return a newly created LFU cache
     */
    @SuppressWarnings("unchecked")
    public LFUCache(int cap, double evictFactor) {
        if (cap <= 0 || evictFactor <= 0 || evictFactor >= 1) {
            throw new IllegalArgumentException("Eviction factor or Capacity is illegal.");
        }
        capacity = cap;
        minFreq = 0;  // the initial smallest frequency
        evictNum = Math.min(cap, (int) Math.ceil(cap * evictFactor));

        store = new HashMap<>();
        accessCountList = new LinkedHashSet[cap];
        for (int i = 0; i < cap; ++i) {
            accessCountList[i] = new LinkedHashSet();
        }
    }

    /**
     * Update access count of the node in the cache if the key exists.
     * Increase the access count of this node and move it to the next counter set.
     * If the access count reaches the capacity, move it the end of current frequency set.
     */
    private synchronized void touch(K key) {
        if (store.containsKey(key)) { // sanity checking
            Node node = store.get(key);
            int id = Math.min(node.count, capacity - 1);
            accessCountList[id].remove(key);
            int newCount = node.touch();
            if (newCount < capacity) {
                store.put(key, node);
                accessCountList[newCount].add(key);
                if (id == minFreq && accessCountList[minFreq].isEmpty()) {
                    // update current minimum frequency
                    ++minFreq;
                }
            } else {
                // LRU: put the most recent visited to the end of set
                accessCountList[id].add(key);
            }
        }
    }

    /**
     * Evict the least frequent elements in the cache
     * The number of evicted elements is configured by eviction factor
     */
    private synchronized void evict() {
        for (int i = 0; i < evictNum && minFreq < capacity; ++i) {
            // get the first element in the current minimum frequency set
            K key = (K) accessCountList[minFreq].iterator().next();
            accessCountList[minFreq].remove(key);
            store.remove(key);
            while (minFreq < capacity && accessCountList[minFreq].isEmpty()) {
                // skip empty frequency sets
                ++minFreq;
            }
        }
    }

    /**
     * Get the value of key.
     * If the key does not exist, return null.
     *
     * @param key the key to query
     * @return the value of the key
     */
    public synchronized V get(K key) {
        if (!store.containsKey(key)) {
            return null;
        }
        // update frequency
        touch(key);
        return store.get(key).v;
    }

    /**
     * Set key to hold the value.
     * If key already holds a value, it is overwritten.
     *
     * @param key   the key of the node
     * @param value the value of the node
     * @return
     */
    public synchronized void set(K key, V value) {
        Node node = store.get(key);
        if (null != node) {
            node.v = value;
            touch(key);  // update frequency
            return;
        }
        if (store.size() >= capacity) {
            evict();
        }
        store.put(key, new Node(value));
        accessCountList[0].add(key);
        // set the minimum frequency back to 0
        minFreq = 0;
    }

    /**
     * Returns the values of all specified keys.
     * For every key that does not exist, null is returned.
     *
     * @param keys a list of keys to query
     * @return query results, a map of key/val extracted
     */
    public synchronized Map mget(List keys) {
        Map ret = new LinkedHashMap<>();
        for (K key : keys) {
            V val = get(key);
            if (null != val) {
                ret.put(key, val);
            }
        }
        return ret;
    }

    /**
     * Sets the given keys to their respective values.
     * MSET replaces existing values with new values, just as regular SET.
     *
     * @param data a map contains the key/val pairs to be set.
     */
    public synchronized void mset(Map data) {
        for (Map.Entry entry : data.entrySet()) {
            set(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Increments the value stored at key by delta.
     * If the key does not exist, it is set to 0 before performing the operation.
     * Only works for integer value.
     * This function will increase frequency by 1
     *
     * @param key   the key needed to be increased
     * @param delta increment
     * @return the value after increment
     */
    @SuppressWarnings("unchecked")
    public synchronized Integer incr(K key, Integer delta) {
        if (!store.containsKey(key)) {
            set(key, (V) delta);
            return delta;
        }
        Node node = store.get(key);
        Integer I = (Integer) node.v;
        if (null == I) {
            I = 0;
        }
        I += delta;
        node.v = (V) I;
        // update frequency
        touch(key);
        return I;
    }

    /**
     * Decrements the value stored at key by delta.
     * If the key does not exist, it is set to 0 before performing the operation.
     * Only works for integer value.
     * This function will increase frequency by 2
     *
     * @param key   the key needed to be decreased
     * @param delta decrement
     * @return the value after decrement
     */
    public synchronized Integer decr(K key, Integer delta) {
        return incr(key, -delta);
    }

    /**
     * Only for testing purpose
     * Print the content of the cache in the order of frequency
     * @return
     */
    public void print() {
        int f = minFreq;
        System.out.println("=========================");
        System.out.println("What is in cache?");
        while (f < capacity) {
            for (Object key : accessCountList[f]) {
                System.out.print("(" + key + ", " + store.get(key).count + " : " + store.get(key).v + "), ");
            }
            ++f;
        }
        System.out.println("\n=========================");
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy