对于无状态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::func的
lambda,并且是具有this实例引用的lambda,情况有所不同。允许JRE缓存它们,但这意味着Map在实际参数值和所得的
lambda之间保持某种形式,这可能比再次创建简单的结构化lambda实例要昂贵得多。当前的JRE不缓存具有状态的
Lambda实例。
但这并不意味着每次都会创建lambda类。这仅意味着已解析的调用站点将像普通的对象构造一样,实例化在第一次调用时生成的
lambda类。
类似的情况适用于对由不同调用站点创建的相同目标方法的方法引用。JRE被允许在它们之间共享一个lambda实例,但是在当前版本中,它不允许共享,这很可能是因为不清楚缓存维护是否会奏效。在这里,即使生成的类也可能不同。
因此,像你的示例一样进行缓存可能会使你的程序做不同的事情。但不一定更有效。缓存的对象并不总是比临时对象更有效。除非你真正衡量了由lambda创建引起的性能影响,否则不应添加任何缓存。
我认为,仅在某些特殊情况下缓存可能有用:
- 我们谈论的是许多使用相同方法的不同呼叫站点
- lambda是在构造函数/类初始化中创建的,因为稍后在使用站点上将
- 同时被多个线程调用
- 遭受第一次调用性能降低的困扰
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)