本文将介绍如何进行 Java Lambdas 序列化性能检测、Lambdas 的重要性以及 Lambdas 在分布式系统中的应用。
Lambdas 表达式是 Java 8 中万众期待的新特性,其若干用途包括:
为匿名内部类减少所需样本代码。
缩小值的作用域。Lambdas 表达式中的 this 不会涉及到外部类,减少了内存泄露。
轻松集成现有 API 与新的 Streams API。
Lambdas 另一个鲜有人知的特点就是可被序列化。
序列化有益于对象的状态持久化和网络传输。 Lambdas 应该尽可能无状态,这样就可以保存 Lambdas ,但这并不是典型的用例。
Lambdas 表达式的目的是给程序库传递代码片段,使之与库中正在运行的程序交互。但是如果程序库支持像 Chronicle Engine 这样的分布式系统,那又会怎么样?
Chronicle Engine 是一个库,可让用户使用各自的应用程序远程访问数据结构,无论是用 Java、C# 客户端,还是用 NFS 文件系统。该库还支持存储和持久化数据,也支持复制。
对于某些局部运行的操作,使用 Lambdas 执行不失为一种简单可行的方法。示例操作如下:
MapView<String, Long> map = acquireMap(“map-name”, String.class, Long.class); map.put(“key”, 1); long ret = map.applyToKey(“key”, v -> v + 1); // ret == 2
这里没有必要知道数据的具体存储位置,如果是远程服务器,就会在那台服务器上对 Lambda 序列化,然后执行,并将结果返回。
上图显示了OneAPM 如何监控和让 Java 应用之间的调用关系可视化。
不获取字段的 Lambda 可以被 Java 更高效地处理,因为每个实例都一样,所以并不需要每次都创建新的对象。但是,若编译时 Lambda 获取到未知值,就需要创建新的对象,并将获取的值保存。
Non capturing Lambda
Function<String, String> appendStar = s -> s + "*"
Capturing Lambda
String star = "*"; Function<String, String> appendStar = s -> s + star;
Lambdas 默认是不可序列化的,必须实现一种可序列化的接口。可以使用强制转换把接口类型转换为所需类型的一种方式。
Function<String, String> appendStar = (Function<String, String> & Serializable) (s -> s + star);
笔者个人不喜欢这样做,因为这样会破坏减少代码的目标。一个解决的方法就是定义自己所需的可序列化的接口。
@FunctionalInterface public interface SerializableFunction<I, O> extends Function<I, O>, Serializable {
这就需要如下所写:
SerializableFunction<String, String> appendStar = s -> s + star;
或者按照这种方法写入:
<R> R applyToKey(K key, @NotNull SerializableFunction<E, R> function) {
该库的调用者就可以如下所写,而不需要任何样本代码。
String s = map.applyToKey(“key”, s-> s + “*”);
利用序列化的 Lambdas,可进行如下所示的实时查询:
// print the last name of all the people in NYC acquireMap(“people”, String.class, Person.class).query() .filter(p -> p.getCity().equals(“NYC”)) // executed on the server .map(p → p.getLastName()) // executed on the server .subscribe(System.out::println); // executed on the client.
可查询接口是必需的,因此过滤器 Predicate 和 map 函数也必须隐式可序列化。如果需要使用 Streams API,那就要使用早期较为复杂的数据类型转换函数 cast。
笔者曾经在一个字符串中写入符号“*”,并使用 JMH 对简单的序列化的和反序列化的 Lambda 进行时延采样,然后比较采集和非采集两种情况下的时延,发送枚举时两种情况下的时延也一并比较。代码和结果如下表所示:
99.99%的时延意味着该试验的99.99%都是在时延之中。时间都用微秒计算。
Test | Typical latency | 99.99% latency |
---|---|---|
Java Serialization, non-capturing | 33.9 µs | 215 µs |
Java Serialization, capturing | 36.3 µs | 704 µs |
Java Serialization, with an enum | 7.9 µs | 134 µs |
Chronicle Wire (Text), non-capturing | 20.4 µs | 147 µs |
Chronicle Wire (Text), capturing | 22.5 µs | 148 µs |
Chronicle Wire (Text), with an enum | 1.2 µs | 5.9 µs |
Chronicle Wire (Binary), non-capturing | 11.7 µs | 103 µs |
Chronicle Wire (Binary), capturing | 12.7 µs | 135 µs |
Chronicle Wire (Binary), with an enum | 1.0 µs | 1.2 µs |
使用 Lambda 是很简单,但它效率不高时,就需要找一个备选方案。所以当 Lambda 的使用造成性能问题时,就要使用备选方案。
enum Functions implements SerializableFunction<String, String> { APPEND_STAR { @Override public String apply(String s) { return s + '*'; } } }
为考察使用枚举所起到的作用,可以比较发送到服务器的数据量的多少,在那里可以看到所有序列化的数据。 下面就是当在 TextWire 中序列化时,非采集的 Lambda 的情况。(基于 YAML)
!SerializedLambda { cc: !type lambda.LambdaSerialization, fic: net/openhft/chronicle/core/util/SerializableFunction, fimn: apply, fims: (Ljava/lang/Object;)Ljava/lang/Object;, imk: 6, ic: lambda/LambdaSerialization, imn: lambda$textWireSerialization$ea1ad110$1, ims: (Ljava/lang/String;)Ljava/lang/String;, imt: (Ljava/lang/String;)Ljava/lang/String;, ca: [ ] }
枚举序列化如下所示:
!Functions APPEND_STAR
注意:当需要采集某些值时,不可以使用枚举。我们要做的就是让你通过传递有附加参数的枚举,以获得最有效的组合。
用枚举代替 Lambdas 的一个好处就是,可以跟踪所有功能客户执行和整合的方式。某些函数使用广泛,运用枚举使得修复任一单独函数中产生的bug更为容易,因此会被经常使用。举一个简单的例子,MapFunction 起初有很多不同的 Lambdas,而现在已经被归为一类。
如果所使用的 API 支持,可将 Lambdas 用于分布式应用程序。如果需要附加的性能,也可以使用枚举。