一尘不染

方法引用缓存在Java 8中是个好主意吗?

java

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

假设hotFunction经常调用。那么建议缓存this::func如下:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

就我对java方法引用的理解而言,使用方法引用时,虚拟机会创建一个匿名类的对象。因此,缓存引用只会创建一次该对象,而第一种方法是在每个函数调用上创建该对象。它是否正确?

是应该缓存出现在代码中热门位置的方法引用,还是VM能够对其进行优化并使多余的缓存?是否有一般的最佳实践,或者这种高度VM实现是否特定于这种缓存是否有用?

Java 缓存 Java-8 方法参考


阅读 426

收藏
2020-03-10

共1个答案

一尘不染

对于无状态Lambda或有状态Lambda ,必须区分相同调用站点的频繁执行和对同一方法的方法引用的频繁使用(由不同的调用站点)之间的区别。

看下面的例子:

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

在这里,相同的调用站点将执行两次,生成无状态的lambda,并且当前的实现将打印出来"shared"

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

在第二个示例中,同一调用站点被执行两次,生成一个包含对Runtime实例的引用的lambda,并且当前实现将打印出来,”unshared”但是”shared class”。

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

相反,在最后一个示例中,有两个不同的调用站点产生等效的方法引用,但从此开始,1.8.0_05它们将打印"unshared"和"unshared class"

对于每个lambda表达式或方法引用,编译器将发出一条invokedynamic指令,该指令引用该类中JRE提供的引导方法LambdaMetafactory以及生成所需lambda实现类所需的静态参数。元工厂生成的内容将留给实际的JRE,但这是invokedynamic指令的指定行为,可以记住并重新使用CallSite在第一次调用时创建的实例。

当前的JRE会为无状态lambda 生成一个ConstantCallSite包含一个MethodHandle对象的常量对象(并且没有可想象的理由进行不同的处理)。并且对方法的方法引用static始终是无状态的。因此,对于无状态的lambda和单个调用站点,答案必须是:不缓存,JVM可以缓存,如果不缓存,则必须有很强的理由不应该抵消。

对于具有参数this::funclambda ,并且是具有this实例引用的lambda,情况有所不同。允许JRE缓存它们,但这意味着Map在实际参数值和所得的lambda之间保持某种形式,这可能比再次创建简单的结构化lambda实例要昂贵得多。当前的JRE不缓存具有状态的Lambda实例。

但这并不意味着每次都会创建lambda类。这仅意味着已解析的调用站点将像普通的对象构造一样,实例化在第一次调用时生成的lambda类。

类似的情况适用于对由不同调用站点创建的相同目标方法的方法引用。JRE被允许在它们之间共享一个lambda实例,但是在当前版本中,它不允许共享,这很可能是因为不清楚缓存维护是否会奏效。在这里,即使生成的类也可能不同。

因此,像你的示例一样进行缓存可能会使你的程序做不同的事情。但不一定更有效。缓存的对象并不总是比临时对象更有效。除非你真正衡量了由lambda创建引起的性能影响,否则不应添加任何缓存。

我认为,仅在某些特殊情况下缓存可能有用:

  • 我们谈论的是许多使用相同方法的不同呼叫站点
  • lambda是在构造函数/类初始化中创建的,因为稍后在使用站点上将
  • 同时被多个线程调用
  • 遭受第一次调用性能降低的困扰
2020-03-10