Java 8是否已缓存对供应商的支持?

Che*_*rry 27 java guava java-8

guava库有它自己的Supplier,不扩展Java 8 Supplier.番石榴还为供应商提供缓存 - Suppliers#memoize.

是否有类似的东西,但对于Java 8供应商?

Tag*_*eev 25

没有内置的Java函数用于memoization,虽然它实现起来并不是很难,例如,像这样:

public static <T> Supplier<T> memoize(Supplier<T> delegate) {
    AtomicReference<T> value = new AtomicReference<>();
    return () -> {
        T val = value.get();
        if (val == null) {
            val = value.updateAndGet(cur -> cur == null ? 
                    Objects.requireNonNull(delegate.get()) : cur);
        }
        return val;
    };
}
Run Code Online (Sandbox Code Playgroud)

注意,存在不同的实现方法.如果被记忆的供应商从不同的线程同时多次请求,则上述实现可以多次调用该代表.有时这种实现比使用lock的显式同步更受欢迎.如果首选锁定,则可以使用DCL:

public static <T> Supplier<T> memoizeLock(Supplier<T> delegate) {
    AtomicReference<T> value = new AtomicReference<>();
    return () -> {
        T val = value.get();
        if (val == null) {
            synchronized(value) {
                val = value.get();
                if (val == null) {
                    val = Objects.requireNonNull(delegate.get());
                    value.set(val);
                }
            }
        }
        return val;
    };
}
Run Code Online (Sandbox Code Playgroud)

另请注意,正如@LouisWasserman在评论中正确提到的那样,您可以使用方法参考轻松地将JDK供应商转换为番石榴供应商,反之亦然:

java.util.function.Supplier<String> jdkSupplier = () -> "test";
com.google.common.base.Supplier<String> guavaSupplier = jdkSupplier::get;
java.util.function.Supplier<String> jdkSupplierBack = guavaSupplier::get;
Run Code Online (Sandbox Code Playgroud)

因此,在Guava和JDK函数之间切换并不是一个大问题.

  • 您可以通过记住另一个(`)-&gt; val`形式的供应商来消除易失性语义。这样,您将使用捕获值的“最终”字段语义。 (3认同)
  • 在这种情况下,您真的不需要 `AtomicReference`,对吗?它似乎只是用作 lambda 可以关闭的可变容器。如果您想保存一个对象分配,我认为您可以返回一个带有可变“值”字段的匿名类实例。对“this”的同步。 (2认同)
  • @Lii,`AtomicReference`只是一个具有单个字段的对象,它提供了易失性读/写语义,这在此是必要的.可以用匿名类volatile字段替换它(仅在第二个示例中,而不是在第一个示例中),但是这种优化是否重要并不是很明显.此外,锁定公共可用对象被认为是一种不好的做法. (2认同)

Hol*_*ger 17

最简单的解决方案是

public static <T> Supplier<T> memoize(Supplier<T> original) {
    ConcurrentHashMap<Object, T> store=new ConcurrentHashMap<>();
    return ()->store.computeIfAbsent("dummy", key->original.get());
}
Run Code Online (Sandbox Code Playgroud)

然而,最简单的并不总是最有效的.

如果你想要一个干净有效的解决方案,诉诸匿名内部类来保持可变状态将会得到回报:

public static <T> Supplier<T> memoize1(Supplier<T> original) {
    return new Supplier<T>() {
        Supplier<T> delegate = this::firstTime;
        boolean initialized;
        public T get() {
            return delegate.get();
        }
        private synchronized T firstTime() {
            if(!initialized) {
                T value=original.get();
                delegate=() -> value;
                initialized=true;
            }
            return delegate.get();
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

这将使用委托供应商,该委托供应商将在第一次操作时执行,之后将其自身替换为无条件返回第一次评估的捕获结果的供应商.由于它具有final字段语义,因此无需任何额外同步即可无条件地返回.

synchronized方法内部firstTime(),仍然initialized需要一个标志,因为在初始化期间并发访问的情况下,多个线程可以在替换委托之前等待方法的条目.因此,这些线程需要检测已经完成了初始化.所有后续访问都将读取新的委托供应商并快速获取值.

  • @CodeConfident:这是在答案的最后一段中解释的.如果多个线程在处于未初始化状态的同时调用`get()`,它们都会读取对初始供应商的引用并尝试输入`firstTime()`方法,除了一个因为`synchronized`.在第一个线程完成初始化之后,挂起的线程将一个接一个地进行,每个线程都必须检测到已经完成了初始化.这是一种罕见的情况,但在一般解决方案中,必须予以处理. (3认同)
  • @Male,讨论是毫无意义的,因为它试图讨论关于实现细节的独立于实现的观点。从 JLS 的角度来看,lambda 没有字段,而仅使用周围上下文的变量。用于保存值副本的字段已经是未指定的实现细节。因此,当 lambda 表达式使用周围上下文的局部变量时,它与第 17.4.1 节相矛盾。它说局部变量“永远不会在线程之间共享”,这显然需要修复。目的是拥有不可变的响应。无状态 lambda,不受种族影响。 (3认同)
  • @glts:`delegate.get()` 将在第一次调用的`synchronized` 方法`firstTime()` 或与`() -&gt; value` lambda 表达式关联的实例中结束,而`value`实际上是最终的。访问捕获的值等同于读取“final”字段,无需额外同步即可安全。如果线程看到`delegate` 引用的陈旧值,它将通过`synchronized``firstTime()` 方法进行一次调用,然后知道最新的值,因此所有后续调用都会进行那么快速路径。 (2认同)
  • 在这种情况下,为什么`delegate`不需要标记为`volatile`? (2认同)
  • @Mark Elliot:捕获的局部变量的值具有“ final”字段语义,因此,如果线程遇到新的委托,即“()-&gt; value”,它也会正确读取“ value”。由于缺少“ volatile”,线程可能会遇到旧的“ delegate”,但是在这种情况下,它将进入“ synchronized”方法“ firstTime()”并读取“ initialized”的最新值,并且代表 每个线程最多可能发生一次,因为此后它具有最新值。 (2认同)
  • @CodeConfident:您可以将委托视为几乎为零的开销,但是,您不应该无缘无故地使用具有更高复杂性的代码,即,如果确实有可能根本不需要该值,或者如果您预计会有明显的延迟在“Supplier”的构建和第一次实际值访问之间。对于普通情况,您应该只计算构造函数中的值(如果是类)或在捕获之前计算 (`T value =calculate();Supplier&lt;T&gt; s = () -&gt; value;`) (2认同)
  • @iMysak如果该功能对您很重要,那么您必须选择命名嵌套类。但是,我认为这并不重要,因为即使您将这样的供应商包装到另一个供应商中,原始供应商也只会被调用一次。我宁愿质疑应用程序设计,因为它盲目地为任意供应商调用“memoize”,而不知道它是否会得到回报。 (2认同)
  • 对单个元素使用并发的哈希图来获取锁对我来说似乎是巨大的浪费。 (2认同)
  • @Alex,它不是为了获取锁,而是为了获取初始化锁和后续的无锁访问。正如答案已经说过的,基于地图的解决方案是最简单的,但“最简单的并不总是最有效的”。但不要高估 `ConcurrentHashMap` 的开销,毕竟它只是一个对象,无论它提供什么*功能*。您是否知道在参考实现中,每个“HashSet”都是“HashMap”的包装器?*这就是*我所说的开销,尽管如此,我们都已经忍受了二十年了…… (2认同)
  • @Male我不明白第二点是如何错误的。JLS 明确禁止凭空取值,因此如果 lambda(引用)不是新的,那么它一定是旧的,这确实会调用“synchronized”方法。lambda 表达式可能指向可变对象,但是将缓存提供者与后续修改相结合本身就是一个错误,无论缓存解决方案如何。所以这在这里无关紧要。lambda 与字符串一样不可变,字符串包含对可变数组的引用。现在我真的希望有人能实现一个矛盾的 JRE 来与 JLS 作者讨论 (2认同)
  • @Male,当线程没有观察到新的“委托”时,它会观察到旧值,因此将进入“同步”方法。这有什么难理解的呢? (2认同)
  • @Male 将字段设为“易失性”并不能保证供应商最多被执行一次。您仍然需要一个锁或“同步”来防止并发执行。一旦执行了“synchronized”方法,您就拥有了可见性保证。当然,仅限于执行了`synchronized`方法的线程,所以在最坏的情况下,每个线程都会执行一次`synchronized`方法。但之后,同一线程的后续读取将读取新值,而无需额外同步。但如果您想要一个简单的解决方案,请查看答案的开头。 (2认同)
  • @Male 当然,JLS 允许观察其他写入,但当然,只有实际存在的其他写入。正如已经说过的,JLS 禁止无中生有的值。那么您所说的“其他写入”是什么?“synchronized”方法的执行会强制执行排序,因此线程“至少会看到该方法之前执行中所做的所有写入”,并且由于该方法“最多执行一次写入”(在第一次执行中),因此后续执行才会意识到只有写。据了解,您不同意捕获的“值”的可见性,但是第二个问题是什么? (2认同)
  • @Yura 只要“Supplier”存在,“ConcurrentHashMap”就会存在。所以供应商比其他解决方案消耗更多的内存,但这并不是内存泄漏。此外,像“new Supply&lt;T&gt;() { ..}”这样的构造只定义了一个类。就像每个 lambda 表达式都使用当前实现生成一个类一样。在实践中,您不会注意到与其他解决方案(例如捕获“AtomicReference”)的解决方案有显着差异。但如果您想要提高效率,请首先消除应用程序逻辑中对缓存供应商的需求。 (2认同)

Koh*_*aki 5

Java 8 上 Guava 20 的简单包装器:

static <T> java.util.function.Supplier<T> memoize(java.util.function.Supplier<? extends T> supplier) {
    return com.google.common.base.Suppliers.memoize(supplier::get)::get;
}
Run Code Online (Sandbox Code Playgroud)