I have the following implementation of an LFU - least frequently used - cache. In case there is a tie between elements which have the same use count then the timestamp is used as a tie breaker to evict the least recently used element
public class LFUCache {
    class CacheKey {
        private int counter;
        private long timestamp;
        private int value;
        public CacheKey(int counter, long timestamp, int value) {
            this.counter = counter;
            this.timestamp = timestamp;
            this.value = value;
        }
        long getTimestamp() {
            return this.timestamp;
        }
        int getCounter() {
            return this.counter;
        }
        void incrementCounter() {
            this.counter++;
        }
        void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
        void setValue(int value) {
            this.value = value;
        }
        public boolean equals(Object o) {
            CacheKey other = (CacheKey) o;
            return other.counter == this.counter && other.timestamp == this.timestamp && this.value == other.value;
        }
        public int hashCode() {
            return Objects.hash(this.counter, this.timestamp, this.value);
        }
        @Override
        public String toString() {
            return "CacheKey{" +
                    "counter=" + counter +
                    ", timestamp=" + timestamp +
                    ", value=" + value +
                    '}';
        }
    }
    /**
     * Map from cache key to its corresponding key
     */
    TreeMap<CacheKey, Integer> timeKeeper;
    /**
     * Map from keys to the keys of th timekeeper map
     */
    Map<Integer, CacheKey> cache;
    /**
     * Total capacity of the cache
     */
    int capacity;
    /**
     * Compare first on the least frequently used, so we are keeping the elements of the cache with the smallest
     * counters in the first entries of the tree map.
     * <p>
     * In case of a tie we have to evict the least recently used, so we are using the timestamp entry, and we are keeping
     * the ones with the smaller
     */
    public LFUCache(int capacity) {
        this.timeKeeper = new TreeMap(Comparator.comparingInt(CacheKey::getCounter).thenComparingLong(CacheKey::getTimestamp));
        this.cache = new HashMap();
        this.capacity = capacity;
    }
    public int get(int key) {
        if (this.cache.containsKey(key)) {
            CacheKey searchKey = this.cache.get(key);
            // We have to remove the old entry from the timekeeper map and re add it to the tree map
            // since if we update only the reference the sorting will not take place again
            this.timeKeeper.remove(searchKey);
            // Update the entry in the timekeeper map with an updated counter and a new last used timestamp
            searchKey.incrementCounter();
            searchKey.setTimestamp(System.currentTimeMillis());
            this.timeKeeper.put(searchKey, key);
            return this.cache.get(key).value;
        } else return -1;
    }
    public void put(int key, int value) {
        // Check if we have to remove something from the map
        if (this.cache.size() == this.capacity) {
            // Remove the least frequently used OR - in case of a tie - least recently used element
            Map.Entry<CacheKey, Integer> lfuEntry = this.timeKeeper.pollFirstEntry();
            CacheKey lfuKey = lfuEntry.getKey();
            Integer lfuValue = lfuEntry.getValue();
            // Remove elements from both cache and timekeeper
            this.timeKeeper.remove(lfuKey);
            this.cache.remove(lfuValue);
        }
        // Check if it is present
        if (this.cache.containsKey(key)) {
            CacheKey searchKey = this.cache.get(key);
            // Remove the old entry from the timekeeper map. Again here we remove the old entry from the timekeeper map
            // and re add it to the tree map since if we update only the reference the sorting will not take place
            this.timeKeeper.remove(searchKey);
            // Update the entry in the timekeeper map with an updated counter and a new last used timestamp
            searchKey.incrementCounter();
            searchKey.setTimestamp(System.currentTimeMillis());
            searchKey.setValue(value);
            this.timeKeeper.put(searchKey, key);
            this.cache.put(key, searchKey);
        } else {
            // Now add the new element
            doPut(key, value);
        }
    }
    private void doPut(int key, int value) {
        CacheKey cacheKey = new CacheKey(1, System.currentTimeMillis(), value);
        this.timeKeeper.put(cacheKey, key);
        this.cache.put(key, cacheKey);
    }
}
I have the following test to verify that everything is working as expected
    public static void main(String[] args) {
            LFUCache lfuCache = new LFUCache(2);
            lfuCache.put(1, 1);
            lfuCache.put(2, 2);
            System.out.println(lfuCache.get(1));
            lfuCache.put(3, 3);
            System.out.println(lfuCache.get(2));
            System.out.println(lfuCache.get(3));
            lfuCache.put(4, 4);
            System.out.println(lfuCache.get(1));
            System.out.println(lfuCache.get(3));
            System.out.println(lfuCache.get(4));
    }
The results I am getting are random. I would expect to get
1
-1
3
-1
3
4
and I do get this sometimes but I also get
1
2
3
-1
3
4
or
1
-1
3
-1
3
4
and all sorts of other results. I tried to create synchronized methods
public synchronized int get(int key) {...}
public synchronized void put(int key, int value) {...}
and I still get random results I tried to synchronize blocks inside methods e.g.
    public int get(int key) {
        if (this.cache.containsKey(key)) {
            synchronized(this){
                CacheKey searchKey = this.cache.get(key);
                // Remove the old entry from the timekeeper map
                this.timeKeeper.remove(searchKey);
                // Create a new entry in the timekeeper map with an updated counter and a new last used timestamp
                CacheKey updatedKey = new CacheKey(searchKey.getCounter() + 1, System.currentTimeMillis(), searchKey.value);
                this.timeKeeper.put(updatedKey, key);
                return this.cache.get(key).value;
            }
            
        } else return -1;
    }
I still get random results
I tried to synchronized on the timeKeeper and cache objects e.g.
    public int get(int key) {
        if (this.cache.containsKey(key)) {
            synchronized(this.timeKeeper){
                synchronized(this.cache){
                    CacheKey searchKey = this.cache.get(key);
                    // Remove the old entry from the timekeeper map
                    this.timeKeeper.remove(searchKey);
                    // Create a new entry in the timekeeper map with an updated counter and a new last used timestamp
                    CacheKey updatedKey = new CacheKey(searchKey.getCounter() + 1, System.currentTimeMillis(), searchKey.value);
                    this.timeKeeper.put(updatedKey, key);
                    return this.cache.get(key).value;
                }
            }
        } else return -1;
    }
but I still get random results
I also tried to use a SyncronizedMap instead of a HashMap but I still get random results.
I cannot really use Collections.synchronizedMap(TreeMap) because this will yield a Map and I cannot use the TreeMap API on it.
So what other options do I have here?
This is a single threaded environment. It's just a main method. How can I have random results being produced?