Search code examples
javacachingweak-referencesweakhashmap

How can I cache where a value has a strong reference to its key?


I have class whose instances can create a self-wrapped copy of its own.

class Some {

    static class Nullable extends Some {

        Nullable(Some wrapped) { this.wrapped = wrapped; }

        @Override public void doSome() {}

        final Some wrapped;
    }

    Some nullable() {
        new Nullable(this);
    }

    public void soSome() {}
}

Now I need to implement some caching for a Some value and its nullable() value.

I found I shouldn't use the WeakHashMap<Some, Some> cuz each has a strong reference to the key(wrapped), right?

How can I implement caching with/without changing the Some class, without any 3rd party library?

I'm trying to do like this. Is this the way?

    private static final Map<Some, Some> MAP = new HashMap<>(); // TODO: synchronize!

    private static final ReferenceQueue<Some> QUEUE = new ReferenceQueue<>();

    private static volatile Thread thread = null;

    @SuppressWarnings({"unchecked"})
    static Some get(final Some some) {
        synchronized (TheClass.class) {
            if (thread == null) {
                thread = new Thread(() -> {
                    while (true) {
                        try {
                            Reference<? extends Some> reference = QUEUE.remove();
                            Some key = reference.get(); // can be null?
                            Some value = MAP.remove(key);
                        } catch (final InterruptedException ie) {
                            Thread.currentThread().interrupt();
                        }
                    }
                });
                thread.setDaemon(true);
                thread.start();
            }
        }
        return (T) MAP.computeIfAbsent(some, k -> {
            final boolean enqueued = new WeakReference<>(k, QUEUE).enqueue();
            assert enqueued;
            return k.nullable();
        });
    }

Follow-up question. Can the reference.get() part really return null?


Solution

  • The true answer depends on how you want your cache to behave. Typically you introduce a cache to prevent having to load the data from some expensive source repeatedly. And for the cache to not grow unlimited, you would add management functions setting the maximum size, time to live or removing the oldest entries when new ones are added.

    As long as your cache keeps normal references to the cached objects, the garbage collector won't evict them from memory. But when memory becomes the limit, would you like your cache to shrink? Can the garbage collector decide to remove some of the objects?

    To allow such behaviour, change the 'normal references' into SoftReference, WeakReference or even PhantomReference. Which of them is best depends on your requirements. I'd probably go for SoftReferences, as the documentation says:

    Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand. Soft references are most often used to implement memory-sensitive caches.

    See https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ref/package-summary.html

    Followup answer: Lookup the documentation for Reference.get():

    Returns: The object to which this reference refers, or null if this reference object has been cleared