使用 Wea​​kReference 的并发缓存抛出 NPE

Concurrent cache using WeakReference's throws an NPE

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

我还需要在没有其他对象引用它们时立即对对象进行 GC(即尽可能低地保持内存占用),所以我想使用 WeakReference,而不是 SoftReference的。

在下面的工厂方法示例中,T 不是通用类型——相反,它可以被认为是具有 id 字段的任意 class输入 String,其中所有 ID 都是唯一的。每个值(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();
}

WeakReference 的主题在清除后需要进行 GC(即引用对象 GC)超出了这个问题的范围——在生产代码中,这个使用引用队列实现。

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


现在,问题。

在 运行 代码成功运行几周后,我收到了一个 NPE,它源自额外的 null 检查 IntelliJ IDEA 添加感谢 @NotNull 注释:

java.lang.IllegalStateException: @NotNull method com/example/T.from must not return null

实际上,这意味着 instanceRef 值没有在任何一个分支中设置,或者整个 computeIfPresent(...) 方法没有被调用。

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

我缺少的竞争条件是否有额外的空间?

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

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

如果在 Here 点启动 GC,您认为会产生什么影响?

我怀疑你的错在@NotNull因为这个方法可以returnnull.

已添加 - 逻辑

如果最后的 instanceRef.get() 是 returning 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. 密钥 不存在

因此,如果实例在调用 putIfAbsent 时存在但在执行 computeIfPresent 时消失,则可能会出现问题。如果在 putIfAbsentcomputeIfPresent 之间删除了一个项目,就会发生这种情况。然而,找到一条 returns 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;
}