2
\$\begingroup\$

I am trying to implement a thread-safe LFU cache algorithm, please review my code and would really appreciate some criticism or suggestion. (on naming, design, data structures used, anything)

Some assumptions:

  1. capacity will be larger than 0.
  2. If the cache is full and multiple keys have the same frequency, the least recently used will be evicted.

Here is the code:

import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

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

public class LFU<K, V> implements CacheAlgorithm<K, V> {

    private final ReentrantLock lock;
    private final int capacity;
    private final TreeMap<Integer, Set<K>> frequencyToKeys;
    private final Map<K, V> entries;
    private final Map<K, Integer> frequencies;

    public LFU(int capacity) {
        this.capacity = capacity;
        lock = new ReentrantLock();
        frequencyToKeys = new TreeMap<>();
        entries = new HashMap<>();
        frequencies = new HashMap<>();
    }

    @Override
    public V get(K key) {
        try {
            V value = entries.get(key);
            if (value != null) {
                incrementFrequency(key);
            }
            return value;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void put(K key, V value) {
        try {
            if (entries.containsKey(key)) {
                incrementFrequency(key);
                entries.put(key, value);
            } else {
                if (capacity == entries.size()) {
                    removeLeastFrequentEntry();
                }
                addNewEntry(key, value);
            }
        } finally {
            lock.unlock();
        }
    }

    private void addNewEntry(K key, V value) {
        entries.put(key, value);
        setKeyFrequency(key, 1);
    }

    private void incrementFrequency(K key) {
        int frequency = frequencies.get(key);
        Set<K> keys = frequencyToKeys.get(frequency);
        keys.remove(key);
        if (keys.isEmpty()) {
            frequencyToKeys.remove(frequency);
        }
        setKeyFrequency(key, frequency + 1);
    }

    private void setKeyFrequency(K key, int frequency) {
        frequencies.put(key, frequency);
        frequencyToKeys.putIfAbsent(frequency, new LinkedHashSet<>());
        frequencyToKeys.get(frequency).add(key);
    }

    private void removeLeastFrequentEntry() {
        Map.Entry<Integer, Set<K>> entry = frequencyToKeys.firstEntry();
        Integer frequency = entry.getKey();
        Set<K> keys = entry.getValue();
        K keyToEvict = keys.iterator().next();
        keys.remove(keyToEvict);
        if (keys.isEmpty()) {
            frequencyToKeys.remove(frequency);
        }
        frequencies.remove(keyToEvict);
        entries.remove(keyToEvict);
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ "It is recommended practice to always immediately follow a call to lock with a try block…" \$\endgroup\$
    – greybeard
    Commented Dec 7, 2020 at 11:51
  • \$\begingroup\$ As presented, "Java" complains about two public in one "file". \$\endgroup\$
    – greybeard
    Commented Dec 7, 2020 at 11:53

1 Answer 1

2
\$\begingroup\$

Few comments from my side:

Looks like a code bug: I think you want to use ReentrantLock to manage concurrent operations in multithreaded environment. But in your code I don't see any place where you have made a call to lock (neither lock() nor tryLock(long timeout, TimeUnit unit)). While unlock() is used couple of time, which is of no use in this case.

Other suggested updated:

  1. CacheAlgorithm interface should be rather renamed to Cache only. Because it doesn't make sense in any class to make use of word Algorithm.

  2. Code should prevent using abbreviations as much as possible. So LFU should be renamed to LeastFrequentlyUsed. Which is not a very big name. Along that it should be ended with Cache to make it more clear to user. So the better name would be LeastFrequentlyUsedCache.

  3. It would be better to have a default constructor with default capacity, as Java API has for Collection classes.

  4. Avoid auto-boxing and auto-unboxing in code. As these are heavy operations. Rather do custom boxing and unboxing as below:


final int frequency = frequencies.get(key).intValue();
final Set<K> keys = frequencyToKeys.get(Integer.valueOf(frequency));
\$\endgroup\$
4
  • \$\begingroup\$ Thanks a lot for your comment, especially for the performance of boxing/unboxing, which I never thought of. For boxing/ unboxing, I googled a bit and it says JVM implicitly calls Integer.valueOf() and intValue() respectively, is there some performance impact for not explicitly boxing/ unboxing interger that I failed to realize? \$\endgroup\$
    – yploo
    Commented Dec 8, 2020 at 7:38
  • \$\begingroup\$ Also, does it make sense to change to Integer frequency = frequencies.get(key) to avoid unnecessary boxing later in the code? (can't escape the unboxing/boxing for frequency setKeyFrequency(key, frequency + 1) i guess) \$\endgroup\$
    – yploo
    Commented Dec 8, 2020 at 7:44
  • \$\begingroup\$ Also, should i make it private void setKeyFrequency(K key, Integer frequency) to avoid boxing in that function ? \$\endgroup\$
    – yploo
    Commented Dec 8, 2020 at 7:45
  • \$\begingroup\$ @yploo Yes as much as possible avoid boxing and unboxing in code. \$\endgroup\$ Commented Dec 8, 2020 at 15:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.