Runnables与方法引用以及垃圾回收

3

Java 8中有一篇相对较新的关于用Actor替代Executor的文章指出,可以使用匿名内部类Runnable来实现:

// Functional operation
executor.execute(new Runnable() {
    System.out.println(data);
});

可以用更加垃圾回收友好的技术来替换,执行相同的操作:
Actor<String> actor = new Actor<>(parentExecutor, ::onMessage);

// Equivalent functional operation
actor.act("Hello world");

public void onMessage(String message) {
   System.out.println(message);
}

有道理的是,通常应该使用方法引用而不是匿名内部类(即new Runnablenew Callable)。这个想法已经存在一段时间了。使用这种 Actor 模式似乎还有另一个更高效的细节,但文章中并没有明确解释。

我明白使用:

executor.execute(() -> System.out.println(data));

这种方式看起来效率似乎会稍微降低,因为编译器必须在调用时创建一个生成的方法invokedynamic“调用站点”并引用它。

这里的优势是什么呢?(1)直接方法引用不需要invokedynamic方法生成的开销,(2)还可以让你将方法参数传递给这个直接引用?这篇文章提出的这个想法,与我们已经使用和了解的模式相比,有哪些关键的要点?


1
第一个链接的内容是错误的。它并不比原来更加GC友好,因为它仍然需要为每个方法引用创建一个对象。它只是用不同类型的对象替换了匿名类对象。它仍然比匿名类更好,因为方法引用或lambda不会创建新的类,这是你第二个链接的重点,但第一个链接中关于改进GC的观点是错误的。不要相信你在网上找到的所有东西。 - Andreas
@Andreas 如果你所说的是真的,那么我感到很惊讶;因为我认为Clebert Suconic——这篇文章的作者和新版ActiveMQ的主要Apache提交者——在发表言论之前一定会做好功课。我只是基于它是否更加GC友好来发表看法。 - Dovmo
@Andreas 匿名类捕获封闭对象,而 lambda 表达式或方法引用仅捕获它们所需的内容。也许这就是他们所说的更加 GC 友好的含义? - maaartinus
如果这就是他们的意思,那么他们应该:1)使用有效的Java语法,2)不使用“this :: onMessage”这样的实例方法引用,因为它也需要一个this引用,即没有任何收益。这就是我说这篇文章是错误的原因,因为使用实例方法引用(在::之前使用对象),必须为lambda创建一个新对象,这意味着它在GC方面与匿名类对象一样。 - Andreas
1个回答

2
这篇文章没有说使用Lambda表达式而不是方法引用会更差。
关键思想是,不是调用
executor.execute(new Runnable() {
    public void run() {
        System.out.println(data);
    }
});

数以百万计的Runnable实例被创建了数百万次,你需要...
Actor<String> actor = new Actor<>(parentExecutor, this::onMessage);

once和call

一次调用
actor.act(data);

数百万次。 Actor 内部使用字符串队列(或您使用的任何数据项),而不将任何项目包装到另一个对象中¹,并使用单个Runnable 进行入队。
在执行相同操作时,也可以正常工作。
Actor<String> actor = new Actor<>(parentExecutor, x -> System.out.println(x));

替代甚至

Actor<String> actor = new Actor<>(parentExecutor, new ActorListener<String>() {
    public void onMessage(String message) {
        System.out.println(message);
    }
});

由于这段代码只会执行一次,因此其技术细节不重要。

虽然Lambda表达式或方法引用使使用此模式更加容易,但它们并不是此模式的基石。文章也没有说错什么。它只是说:“使用Lambda表达式可以变得非常优雅”。


¹若要挑剔,它在幕后使用了ConcurrentLinkedQueue,它确实将每个项包装到一个节点对象中,说明临时对象是多么不重要。使用基于数组的队列会更一致,但是,队列就不能是非阻塞的了。你不能两全其美...


好的,我现在再看代码,明白你的意思了。如果我能用一句话概括它,那就是一个GC友好的生产者-消费者模式实现,通过创建单个“Actor”并通过该抽象管理同步来提供语法糖。我喜欢这个想法。谢谢!我希望其他人也能从中受益。 - Dovmo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接