简体   繁体   中英

Java SoftReference strange behaviour

Map<E, SoftReference<T>> cache = new ConcurrentHashMap<E, SoftReference<T>>();

I have map declared a map like the above one which I'm using as a Cache.

The problem is I'm to perform all operations on the Cache immediately after adding an item to the Cache but not later.

For ex:

cache.add("Username", "Tom");

if(cache.contains("Username")) returns true but

String userName = (String)cache.get("Username") returns null.

This happens only after a long time.

If I get the value after a few hours of adding it to the cache, I get the value correctly. If I get the value after a long time, say more than 15-20 hrs, I get null.

When GC clears SoftReference objects, will the key remain in HashMap? Is that the reason for this behaviour?

When the referent of a SoftReference gets garbage collected, the SoftReference gets cleared , ie its referent field is set to null .

So not only does the key stay in the map, the associated value, the SoftReference instance, also stays in the map, even if its referent field is null .

But since there must be a layer between your declared Map<E, SoftReference<T>> field and the caller of cache.add("Username", "Tom") and (String)cache.get("Username") , which you didn't show, it might even be the case that this layer handles it correctly.

For completeness, a correct implementation could look like

final Map<E, SoftReference<T>> cache = new ConcurrentHashMap<>();

/** remove all cleared references */
private void clean() {
    cache.values().removeIf(r -> r.get() == null);
}
public void add(E key, T value) {
    clean();
    cache.put(key, new SoftReference<>(value));
}
public boolean contains(E key) {
    clean();
    return cache.computeIfPresent(key, (e,r) -> r.get()==null? null: r) != null;
}
public T get(E key) {
    clean();
    for(;;) {
        SoftReference<T> ref = cache.computeIfPresent(key, (e,r) -> r.get()==null? null: r);
        if(ref == null) return null;
        T value = ref.get();
        if(value != null) return value;
    }
}

This code ensures that mappings of collected values get removed atomically when queried. Additionally, though not required, the clean() operation removes all collected entries of the map, to reduce the map's space (comparable to how WeakHashMap works internally).

But note that still, there is no guaranty that when cache.contains("Username") returns true , a subsequent cache.get("Username") would return a non- null value. This problem is also known as the check-then-act anti-pattern, which may fail because between the check and the subsequent action, a concurrent update may happen (here even if you're using the cache by only one thread, as the garbage collection may happen asynchronously), outdating the result of the preceding test.

In this regard, the contains operation is useless for most scenarios. You have to invoke get , to receive a strong reference to a value, and proceed with that reference, if non- null .

As per oracle docs

All soft references to softly-reachable objects are guaranteed to have been cleared before the virtual machine throws an OutOfMemoryError.

Yes When GC clears SoftReference objects, the key remain in HashMap. The key and corresponding value have no relation other than when they are inside a map. Make map's value a normal reference and they will always be in the map unless map is GC.

Yes, that is normal behaviour.

The SoftReference is garbage collected, resulting in the value in the Map being set to null .

It is the same as setting the value of a certain key to null for other types of maps (eg. Map)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM