通过 #compute 将 WeakReferences 添加到 HashMap 中 - 我可以得到 null 吗?

Adding WeakReferences into HashMap via #compute - may I get null or not?

提问人:Andrey B. Panfilov 提问时间:4/26/2023 更新时间:4/29/2023 访问量:87

问:

假设我有以下缓存实现,旨在将一些数据(在我的情况下是连接池)与另一个对象的最新状态/版本相关联:

public class Demo<V> {

    private final Map<Integer, VAR<V>> cache = new ConcurrentHashMap<>();

    private final ReferenceQueue<V> refq = new ReferenceQueue<>();

    private final Function<Integer, V> factory;

    public Demo(Function<Integer, V> factory) {
        this.factory = factory;
    }

    public static void main(String[] args) throws Exception {
        Demo<Object> demo = new Demo<>(id -> new Object());

        Object data = demo.getCached(1, 0);
        assert data != null;

        WeakReference<Object> keyReference = new WeakReference<>(data);
        data = null;
        while (keyReference.get() != null) {
            System.gc();
            Thread.sleep(1_000);
        }

    }

    public V getCached(int id, int version) {
        assert version >= 0;
        VAR<V> ref = cache.get(id);
        V storedData = ref == null ? null : ref.get();
        int storedVersion = storedData == null ? -1 : ref.getVersion();

        if (storedData == null) {
            cache.remove(id, ref);
        }

        if (storedVersion >= version) {
            return storedData;
        }

        Supplier<VAR<V>> varFactory = () -> {
            V data = factory.apply(id);
            return new WeakVAR(data, id, version);
        };

        BiFunction<Integer, VAR<V>, VAR<V>> replaceFunction = (key, existing) -> {
            V data = existing == null ? null : existing.get();
            if (data == null) {
                return varFactory.get();
            }
            if (existing.getVersion() >= version) {
                return existing;
            }
            return varFactory.get();
        };

        Supplier<V> vFactory = () -> cache.compute(id, replaceFunction).get();

/*
        V result;
        while ((result = vFactory.get()) == null) {
            // nop
        }
        return result;
*/

        return vFactory.get();
    }


    interface VAR<V> {

        int getVersion();

        int getId();

        V get();

    }

    class WeakVAR extends WeakReference<V>
            implements VAR<V> {

        private final int id;

        private final int version;

        public WeakVAR(V referent, int id, int version) {
            super(referent, refq);
            this.id = id;
            this.version = version;
        }

        @Override
        public int getVersion() {
            return version;
        }

        @Override
        public int getId() {
            return id;
        }
    }

}

问题是:取消引用返回时,方法是否有可能返回 null?如果是这样,克服这个问题的最佳方法是什么?繁忙循环是一个好的解决方案吗?public V getCached(int id, int version)WeakReferenceMap#compute

Java 垃圾回收 弱引用

评论


答:

4赞 Holger 4/29/2023 #1

是的,您的方法可能会返回 .原则上,当你有这样的代码时null

WeakReference<Object> ref = new WeakReference<>(new Object());
Object o = ref.get();

这已经有可能了.onull

不应创建循环,而应确保在检查现有对象或创建新对象与最终返回语句之间没有结果对象只能微弱访问的阶段。

或者,换言之,强制在整个操作过程中强制对象是强可访问的。

例如

public V getCached(int id, int version) {
    // do not abuse assertions for argument checking
    if(version < 0) throw new IllegalArgumentException();

    VAR<V> ref = cache.get(id);
    V storedData = ref == null? null: ref.get();

    if(storedData != null && ref.getVersion() >= version) {
        return storedData;
    }

    List<V> strongReference = new ArrayList<>(1);

    cache.compute(id, (key, existing) -> {
        V data = existing == null? null: existing.get();
        if(data != null && existing.getVersion() >= version) {
            strongReference.add(data);
            return existing;
        }
        data = factory.apply(key);
        strongReference.add(data);
        return new WeakVAR(data, key, version);
    });

    return strongReference.get(0);
}

开头的快速路径不需要额外的努力,因为变量已经是一个强引用,它始终用于检查旧对象的存在并返回它。storedData

传递给方法的函数必须返回弱引用以符合映射的类型,因此,它需要额外的存储来在通过方法返回时保持强引用。该示例使用但任何带有引用变量的本地对象都可以。computecomputeArrayList

通常,单个元素数组用于此目的,但 Java 不支持创建数组。最后,开销并不重要,只要代码在快速路径上成功,并且足够乐观。V[]get()

评论

0赞 Andrey B. Panfilov 4/29/2023
谢谢,实际上我也在考虑这个选项,但是,方法的结果是否有可能与重映射功能看到的对象不同?即可能不包含最新版本?#computestrongReference
2赞 Eugene 4/29/2023
那个“可能”,实际上在不久前被发现是JVM代码中的真正缺陷
2赞 Holger 4/29/2023
@AndreyB.Panfilov,在此答案的代码中,该方法的结果将始终与存储在 中的引用同步。但是,您使用 表明涉及多个线程。当其他线程在完成后更新相同的键时,不能保证此方法返回最新版本。但无论如何,这是无法解决的;即使你使整个方法,返回值也可能在返回后的下一个纳秒内变得过时。computestrongReferenceConcurrentHashMapcomputegetCachedsynchronizedgetCached
0赞 Andrey B. Panfilov 4/29/2023
@Holger嗯......根据默认实现,它可以多次调用重映射函数,因此,在返回方法列表时可能包含多个条目,第一个条目(0)将是最过时的。ConcurrentMap#compute#computestrongReference
2赞 Holger 4/29/2023
ConcurrentMap是描述所有实现必须提供的最低保证的接口。实际实施提供了更有力的保障。它的计算方法指出:“整个方法调用是以原子方式执行的。每次调用此方法时,提供的函数都会被调用一次。我之前的评论是在假设您继续使用 .ConcurrentHashMapConcurrentHashMap