download:挪动互联网高级开发正式课 VIP 课程 - 码牛第二期
Java 高性能序列化工具 Kryo 序列化
概述
Kryo 是一个疾速序列化 / 反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有肯定的劣势,但正因如此,其使用也只能限度在基于 JVM 的语言上。
和 Hessian 类似,Kryo 序列化出的后果,是其自定义的、独有的一种格局。因为其序列化出的后果是二进制的,也即 byte[],因此像 Redis 这样可能存储二进制数据的存储引擎是可能间接将 Kryo 序列化进去的数据存进去。当然你也可能抉择转换成 String 的形式存储在其余存储引擎中(性能有损耗)。
基础用法
介绍了这么多,接下来咱们就来看看 Kryo 的基础用法吧。其实对于序列化框架来说,API 基本都差不多,毕竟入参和出参通常都是必定的(需要序列化的对象 / 序列化的后果)。在使用 Kryo 之前,咱们需要引入相应的依赖。
com.esotericsoftware
kryo
5.2.0
复制代码
基本使用如下所示
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import java.io.*;
public class HelloKryo {
static public void main(String[] args) throws Exception {
Kryo kryo = new Kryo();
kryo.register(SomeClass.class);
SomeClass object = new SomeClass();
object.value = "Hello Kryo!";
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeObject(output, object);
output.close();
Input input = new Input(new FileInputStream("file.bin"));
SomeClass object2 = kryo.readObject(input, SomeClass.class);
input.close();
System.out.println(object2.value);
}
static public class SomeClass {
String value;
}
}
复制代码
Kryo 类会主动执行序列化。Output 类和 Input 类负责处理缓冲字节,并写入到流中。
如果序列化前和序列化后类的字段不一致,反序列化会失败。
Kryo 的序列化
作为一个灵活的序列化框架,Kryo 并不关心读写的数据,作为开发者,你可能随便使用 Kryo 提供的那些开箱即用的序列化器。
Kryo 的注册
和很多其余的序列化框架一样,Kryo 为了提供性能和减小序列化后果体积,提供注册的序列化对象类的形式。在注册时,会为该序列化类生成 int ID,后续在序列化时使用 int ID 唯一标识该类型。注册的形式如下:
kryo.register(SomeClass.class);
复制代码
或者
kryo.register(SomeClass.class, 1);
复制代码
可能明确指定注册类的 int ID,然而该 ID 必须大于等于 0。如果不提供,外部将会使用 int++ 的形式保护一个有序的 int ID 生成。
Kryo 的序列化器
Kryo 反对多种序列化器,通过源码咱们可窥知一二
具体可参考 👉「Kryo 反对的序列化类型」
诚然 Kryo 提供的序列化器可能读写大多数对象,但开发者也可能轻松的制订自己的序列化器。篇幅限度,这里就不开展说明了,仅以默认的序列化器为例。
对象引用
在新版本的 Kryo 中,默认情况下是不启用对象引用的。这意味着如果一个对象多次出现在一个对象图中,它将被多次写入,并将被反序列化为多个不同的对象。
举个例子,当开启了引用属性,每个对象第一次出现在对象图中,会在记录时写入一个 varint,用于标记。当此后有同一对象出现时,只会记录一个 varint,以此达到俭约空间的目标。此举固然会俭约序列化空间,然而是一种用工夫换空间的做法,会影响序列化的性能,这是因为在写入 / 读取对象时都需要进行追踪。
开发者可能使用 kryo 自带的 setReferences 方法来决定是否启用 Kryo 的引用功能。
public class KryoReferenceDemo {
public static void main(String[] args) throws FileNotFoundException {Kryo kryo = new Kryo();
kryo.register(User.class);
kryo.register(Account.class);
User user = new User();
user.setUsername("alvin");
Account account = new Account();
account.setAccountNo("10000");
// 循环引用
user.setAccount(account);
account.setUser(user);
// 这里需要设置为 true,才不会报错
kryo.setReferences(true);
Output output = new Output(new FileOutputStream("kryoreference.bin"));
kryo.writeObject(output, user);
output.close();
Input input = new Input(new FileInputStream("kryoreference.bin"));
User object2 = kryo.readObject(input, User.class);
input.close();
System.out.println(object2.getUsername());
System.out.println(object2.getAccount().getAccountNo());
}
public static class User {
private String username;
private Account account;
public String getUsername() {return username;}
public void setUsername(String username) {this.username = username;}
public Account getAccount() {return account;}
public void setAccount(Account account) {this.account = account;}
}
public static class Account {
private String accountNo;
private String amount;
private User user;
public String getAccountNo() {return accountNo;}
public void setAccountNo(String accountNo) {this.accountNo = accountNo;}
public String getAmount() {return amount;}
public void setAmount(String amount) {this.amount = amount;}
public User getUser() {return user;}
public void setUser(User user) {this.user = user;}
}
}
复制代码
如果序列化前的 setReferences(false),前面设置 setReferences(true)进行反序列化,会失败。
线程不安全
Kryo 不是线程安全的。每个线程都应该有自己的 Kryo 对象、输出和输入实例。
因此在多线程环境中,可能考虑使用 ThreadLocal 或者对象池来保障线程安全性。
ThreadLocal + Kryo 解决线程不安全
ThreadLocal 是一种典型的就义空间来换取并发安全的形式,它会为每个线程都单首创立本线程专用的 kryo 对象。对于每条线程的每个 kryo 对象来说,都是次序执行的,因此天然避免了并发安全问题。创建方法如下:
static private final ThreadLocal kryos = new ThreadLocal() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
// 在此处配置 kryo 对象的使用示例,如循环引用等
return kryo;
};
};
Kryo kryo = kryos.get();
复制代码
之后,仅需要通过 kryos.get() 方法从线程上下文中取出对象即可使用。
对象池 + Kryo 解决线程不安全
「池」是一种非常重要的编程思维,连接池、线程池、对象池等都是
「复用」思维的体现,通过将创建的“对象”保存在某一个“容器”中,以便后续反复使用,避免创建、销毁的产生的性能损耗,以此达到晋升整体性能的作用。
Kryo 对象池原理也是如此。Kryo 框架自带了对象池的实现,整个使用过程不外乎创建池、从池中获取对象、归还对象三步,以下为代码实例。
// Pool constructor arguments: thread safe, soft references, maximum capacity
Pool kryoPool = new Pool(true, false, 8) {
protected Kryo create () {
Kryo kryo = new Kryo();
// Kryo 配置
return kryo;
}
};
// 获取池中的 Kryo 对象
Kryo kryo = kryoPool.obtain();
// 将 kryo 对象归还到池中
kryoPool.free(kryo);
复制代码
创建 Kryo 池时需要传入三个参数,其中第一个参数用于指定是否在 Pool 外部使用同步,如果指定为 true,则容许被多个线程并发拜访。第三个参数实用于指定对象池的大小的,这两个参数较容易理解,因此重点来说一下第二个参数。
如果将第二个参数设置为 true,Kryo 池将会使用 java.lang.ref.SoftReference 来存储对象。这容许池中的对象在 JVM 的内存压力大时被垃圾回收。Pool clean 会删除所有对象已经被垃圾回收的软引用。当没有设置最大容量时,这可能缩小池的大小。当池子有最大容量时,没有必要调用 clean,因为如果达到了最大容量,Pool free 会尝试删除一个空引用。
创建完 Kryo 池后,使用 kryo 就变得异样简略了,只需调用 kryoPool.obtain() 方法即可,使用完毕后再调用 kryoPool.free(kryo) 归还对象,就实现了一次完整的租赁使用。
实践上,只需对象池大小评估切当,就能在占用极小内存空间的情况下圆满解决并发安全问题。如果想要封装一个 Kryo 的序列化方法,可能参考如下的代码
public static byte[] serialize(Object obj) {
Kryo kryo = kryoPool.obtain();
// 使用 Output 对象池会导致序列化重复的谬误(getBuffer 返回了 Output 对象的 buffer 引用)
try (Output opt = new Output(1024, -1)) {
kryo.writeClassAndObject(opt, obj);
opt.flush();
return opt.getBuffer();
}finally {
kryoPool.free(kryo);
}
}
复制代码
小结
相较于 JDK 自带的序列化形式,Kryo 的性能更快,并且因为 Kryo 容许多引用和循环引用,在存储开销上也更小。
只不过,诚然 Kryo 具备非常好的性能,但其自身却舍去了很多个性,例如线程安全、对序列化对象的字段修改等。诚然这些弊病可能通过 Kryo 良好的扩展性失去肯定的满足,然而对于开发者来说仍然具备肯定的上手难度,不过这并不能影响其在 Java 中的地位。