本文小编为大家详细介绍“Java高性能序列化工具Kryo怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“Java高性能序列化工具Kryo怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
概述
Kryo 是一个快速序列化/反序列化工具,依赖于字节码生成机制(底层使用了 ASM 库),因此在序列化速度上有一定的优势,但正因如此,其使用也只能限制在基于 JVM 的语言上。
和 Hessian 类似,Kryo 序列化出的结果,是其自定义的、独有的一种格式。由于其序列化出的结果是二进制的,也即 byte[],因此像 Redis 这样可以存储二进制数据的存储引擎是可以直接将 Kryo 序列化出来的数据存进去。当然你也可以选择转换成 String 的形式存储在其他存储引擎中(性能有损耗)。
基础用法
介绍了这么多,接下来我们就来看看 Kryo 的基础用法吧。其实对于序列化框架来说,API 基本都差不多,毕竟入参和出参通常都是确定的(需要序列化的对象/序列化的结果)。在使用 Kryo 之前,我们需要引入相应的依赖。
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>5.2.0</version> </dependency>
基本使用如下所示:
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 mainString[] args) throws Exception { Kryo kryo = new Kryo); kryo.registerSomeClass.class); SomeClass object = new SomeClass); object.value = "Hello Kryo!"; Output output = new Outputnew FileOutputStream"file.bin")); kryo.writeObjectoutput, object); output.close); Input input = new Inputnew FileInputStream"file.bin")); SomeClass object2 = kryo.readObjectinput, SomeClass.class); input.close); System.out.printlnobject2.value); } static public class SomeClass { String value; } }
Kryo 类会自动执行序列化。Output 类和 Input 类负责处理缓冲字节,并写入到流中。如果序列化前和序列化后类的字段不一致,反序列化会失败。
Kryo 的序列化
作为一个灵活的序列化框架,Kryo 并不关心读写的数据,作为开发者,你可以随意使用 Kryo 提供的那些开箱即用的序列化器。
Kryo 的注册
和很多其他的序列化框架一样,Kryo 为了提供性能和减小序列化结果体积,提供注册的序列化对象类的方式。在注册时,会为该序列化类生成 int ID,后续在序列化时使用 int ID 唯一标识该类型。
注册的方式如下:
kryo.registerSomeClass.class);
或者
kryo.registerSomeClass.class, 1);
可以明确指定注册类的 int ID,但是该 ID 必须大于等于 0。如果不提供,内部将会使用 int++的方式维护一个有序的 int ID 生成。
Kryo 的序列化器
Kryo 支持多种序列化器,通过源码我们可窥知一二
虽然 Kryo 提供的序列化器可以读写大多数对象,但开发者也可以轻松的制定自己的序列化器。篇幅限制,这里就不展开说明了,仅以默认的序列化器为例。
对象引用
在新版本的 Kryo 中,默认情况下是不启用对象引用的。这意味着如果一个对象多次出现在一个对象图中,它将被多次写入,并将被反序列化为多个不同的对象。
举个例子,当开启了引用属性,每个对象第一次出现在对象图中,会在记录时写入一个 varint,用于标记。当此后有同一对象出现时,只会记录一个 varint,以此达到节省空间的目标。此举虽然会节省序列化空间,但是是一种用时间换空间的做法,会影响序列化的性能,这是因为在写入/读取对象时都需要进行追踪。
开发者可以使用 kryo 自带的 setReferences 方法来决定是否启用 Kryo 的引用功能。
public class KryoReferenceDemo { public static void mainString[] args) throws FileNotFoundException { Kryo kryo = new Kryo); kryo.registerUser.class); kryo.registerAccount.class); User user = new User); user.setUsername"alvin"); Account account = new Account); account.setAccountNo"10000"); // 循环引用 user.setAccountaccount); account.setUseruser); // 这里需要设置为true,才不会报错 kryo.setReferencestrue); Output output = new Outputnew FileOutputStream"kryoreference.bin")); kryo.writeObjectoutput, user); output.close); Input input = new Inputnew FileInputStream"kryoreference.bin")); User object2 = kryo.readObjectinput, User.class); input.close); System.out.printlnobject2.getUsername)); System.out.printlnobject2.getAccount).getAccountNo)); } public static class User { private String username; private Account account; public String getUsername) { return username; } public void setUsernameString username) { this.username = username; } public Account getAccount) { return account; } public void setAccountAccount account) { this.account = account; } } public static class Account { private String accountNo; private String amount; private User user; public String getAccountNo) { return accountNo; } public void setAccountNoString accountNo) { this.accountNo = accountNo; } public String getAmount) { return amount; } public void setAmountString amount) { this.amount = amount; } public User getUser) { return user; } public void setUserUser user) { this.user = user; } } }
如果序列化前的setReferencesfalse), 后面设置setReferencestrue)进行反序列化,会失败。
线程不安全
Kryo 不是线程安全的。每个线程都应该有自己的 Kryo 对象、输入和输出实例。
因此在多线程环境中,可以考虑使用 ThreadLocal 或者对象池来保证线程安全性。
ThreadLocal + Kryo 解决线程不安全
ThreadLocal 是一种典型的牺牲空间来换取并发安全的方式,它会为每个线程都单独创建本线程专用的 kryo 对象。对于每条线程的每个 kryo 对象来说,都是顺序执行的,因此天然避免了并发安全问题。创建方法如下:
static private final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>) { 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<Kryo> kryoPool = new Pool<Kryo>true, false, 8) { protected Kryo create ) { Kryo kryo = new Kryo); // Kryo 配置 return kryo; } }; // 获取池中的Kryo对象 Kryo kryo = kryoPool.obtain); // 将kryo对象归还到池中 kryoPool.freekryo);
创建 Kryo 池时需要传入三个参数,其中第一个参数用于指定是否在 Pool 内部使用同步,如果指定为 true,则允许被多个线程并发访问。第三个参数适用于指定对象池的大小的,这两个参数较容易理解,因此重点来说一下第二个参数。
如果将第二个参数设置为 true,Kryo 池将会使用 java.lang.ref.SoftReference 来存储对象。这允许池中的对象在 JVM 的内存压力大时被垃圾回收。Pool clean 会删除所有对象已经被垃圾回收的软引用。当没有设置最大容量时,这可以减少池的大小。当池子有最大容量时,没有必要调用 clean,因为如果达到了最大容量,Pool free 会尝试删除一个空引用。
创建完 Kryo 池后,使用 kryo 就变得异常简单了,只需调用 kryoPool.obtain) 方法即可,使用完毕后再调用 kryoPool.freekryo) 归还对象,就完成了一次完整的租赁使用。
理论上,只要对象池大小评估得当,就能在占用极小内存空间的情况下完美解决并发安全问题。如果想要封装一个 Kryo 的序列化方法,可以参考如下的代码
public static byte[] serializeObject obj) { Kryo kryo = kryoPool.obtain); // 使用 Output 对象池会导致序列化重复的错误(getBuffer返回了Output对象的buffer引用) try Output opt = new Output1024, -1)) { kryo.writeClassAndObjectopt, obj); opt.flush); return opt.getBuffer); }finally { kryoPool.freekryo); } }