使用WeakReference的并发缓存会抛出NPE

时间:2016-04-26 12:31:08

标签: java concurrency nullpointerexception garbage-collection weak-references

我需要一个并发的对象缓存,其中每个实例都包含一个唯一的id(可能还有一些额外的信息,为了简单起见,在下面的代码片段中省略了),并且不能创建比相应的ID,

一旦没有其他对象引用它们,我也需要GC对象(即保持内存foorprint尽可能低),所以我想使用WeakReference,而不是{{1 }}的

在下面的工厂方法示例中,SoftReference不是通用类型 - 相反,它可以被视为具有T类型id字段的任意类,所有ID都是独一无二的。每个值(类型String)都映射到相应的Reference<T>

id

static final ConcurrentMap<String, WeakReference<T>> INSTANCES = new ConcurrentHashMap<>(); @NotNull public static T from(@NotNull final String id) { final AtomicReference<T> instanceRef = new AtomicReference<>(); final T newInstance = new T(id); INSTANCES.putIfAbsent(id, new WeakReference<>(newInstance)); /* * At this point, the mapping is guaranteed to exist. */ INSTANCES.computeIfPresent(id, (k, ref) -> { final T oldInstance = ref.get(); if (oldInstance == null) { /* * The object referenced by ref has been GC'ed. */ instanceRef.set(newInstance); return new WeakReference<>(newInstance); } instanceRef.set(oldInstance); return ref; }); return instanceRef.get(); } 的主题一旦被清除就需要GC(即引用对象GC'ed)超出了这个问题的范围 - 在生产代码中,这是实现的使用参考队列。

WeakReference仅用于从lambda外部返回一个值(在与工厂方法本身相同的线程中执行)。

现在,问题。

在代码运行成功几周后,我收到AtomicReference来自额外的NPE检查IntelliJ IDEA,这要归功于null注释:

  

java.lang.IllegalStateException:@NotNull方法com / example / T.from必须不返回null

实际上,这意味着未在任何一个分支中设置@NotNull值,或者未调用整个instanceRef方法。

我看到的竞争条件的唯一可能性是在computeIfPresent(...)putIfAbsent(...)调用之间的某个位置删除了映射条目(从单独的线程处理引用队列到GC的实例)。

我缺少一个比赛条件的额外空间吗?

1 个答案:

答案 0 :(得分:1)

你必须记住,不仅可以发生其他线程,还可以发生GC。考虑一下这个片段:

  instanceRef.set(oldInstance);
  return ref;
});
// Here!!!!!
return instanceRef.get();

如果GC在Here点开始,你认为会有什么影响?

我怀疑您的错误在@NotNull,因为此方法可以返回null

已添加 - 逻辑

如果最终instanceRef.get()返回null(如暗示的那样),则可以进行以下陈述。

  1. 密钥 存在且oldInstance已成为GCd。记录肯定非空newInstance

    // This line MUST be executed.
    instanceRef.set(newInstance);
    
  2. 密钥 存在且oldInstance 已成为GCd。记录肯定非空oldInstance

    // This line MUST be executed.
    instanceRef.set(oldInstance);
    
  3. 存在。
  4. 因此,当调用putIfAbsent但在执行computeIfPresent时消失实例时,可能会出现此问题。如果在putIfAbsentcomputeIfPresent之间删除了某个项目,则可能会出现此情况。但是,在没有发生删除时找到返回null的路线很困难。

    可能的解决方案

    您或许可以确保所引用的项目始终记录在参考中。

    @NotNull
    public static Thing fromMe(@NotNull final String id) {
        // Keep track of the thing I've created (if any)
        // Use AtomicReference as a mutable final.
        // NB: Also delays GC as a hard reference is held.
        final AtomicReference<Thing> thing = new AtomicReference<>();
        // Make the map entry if not exists.
        INSTANCES.computeIfAbsent(id,
                // New one only made if not present.
                r -> new WeakReference<>(newThing(thing, id)));
    
        // Grab it - whatever it's contents.
        // NB: Parallel deletions will cause a NPE here.
        trackThing(thing, INSTANCES.get(id).get());
        // Has it been GC'd
        if (thing.get() == null) {
            // Make it again!
            INSTANCES.put(id, new WeakReference<>(newThing(thing, id)));
        }
    
        return thing.get();
    }
    
    // Makes a new Thing - keeping track of the new one in the reference.
    static Thing newThing(AtomicReference<Thing> thing, String id) {
        // Make the new Thing.
        return trackThing(thing, new Thing(id));
    }
    
    // Tracks the Thing in the Atomic.
    static Thing trackThing(AtomicReference<Thing> thing, Thing it) {
        // Keep track of it.
        thing.set(it);
        return it;
    }