JDK内置同步器的实现类经常会看到java.io.Serializable接口,这个接口即是Java序列化操作,这样看来序列化也是同步器的一种机制。
关于序列化
本文主要分析Java中的序列化机制,并看看AQS同步器的序 列化,掌握序列化机制才能完整理解JDK内置的同步工具的实现。
在程序中为了能直接以Java对象的形式进行保存,然后再重新得到该Java对象,我们需要序列化能力。序列化其实可以看成是一种机制,即按照一定的格式将Java对象的状态转成介质可接受的形式,以方便存储或传输。Java中进行序列化操作需要实现Serializable或Externalizable接口。
序列化过程
Java序列化的大致过程为:序列化时将Java对象相关的类信息、属性及属性值等等以一定的格式转为字节流,反序列化时则根据字节流表示的信息来构建出Java对象。过程中可能涉及到其它对象的引用,所以涉及到引用的对象的相关信息也要参与序列化。如下图所示,Java对象经过序列化后转为字节流,保存字节流的常见方式有文件、内存、网络、数据库。反序列化时则通过这些介质读取字节流,然后还原为Java对象。
序列化作用
提供一种简单又可扩展的对象保存恢复机制。
对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
可以将对象持久化到介质中,就像实现对象直接存储。
允许对象自定义外部存储的格式。
序列化的例子
常见的使用方式是直接将对象写入流中,比如下面的例子中,创建了FileOutputStream对象,对应输出到tmp.o文件中。然后创建ObjectOutputStream对象嵌套前面的输出流,当我们调用writeObject方法时即能进行序列化操作。
writeObject方法需要特别说明一下,当我们对某个对象进行写入时,其实不仅仅序列化该对象,它还会去遍历寻找相关引用的其它对象,由自己和其它引用对象组成的一个完整的对象图关系都会被序列化。除了一些特殊指定的类,普通类必须实现Serializable或Externalizable接口才能被序列化。
反序列化的例子
反序列化是序列化的反向操作,即通过字节流来还原Java对象。看下面的例子,首先创建FileInputStream对象,其对应的文件为tmp.o。然后创建ObjectInputStream对象嵌套前面的输入流,接着则可以调用readObject方法读取对象。readObject方法除了会恢复对象自己之外,它还会遍历整个完整的对象图关系,创建整个对象图包含的所有对象。
serialVersionUID的作用
在 序列化操作时,经常会看到实现了Serializable接口的类会存在一个serialVersionUID属性,并且它是一个固定数值的静态变量。 比如下面的一行代码,这个属性有什么作用? 其实它主要用于验证版本的一致性。 每个类都拥有这么一个ID,在序列化的时候会一起被写入流中。 在反序列化的时候就被拿出来跟当前类的serialVersionUID值进行比较,两者相同则说明版本一致,可以序列化成功,而如果不同则序列化失败。
一般情况下我们可以自己定义serialVersionUID的值或者由IDE帮我们自动生成,如果我们不显示定义serialVersionUID的话,也不代表不存在serialVersionUID,而是由JDK帮我们生成。生成规则是利用类名、类修饰符、接口名、字段、静态初始化信息、构造函数信息、方法名、方法修饰符、方法签名等组成的信息,经过SHA算法生成摘要作为最终的serialVersionUID值。
父类的序列化
如果一个子类实现了Serializable接口而父类没有实现该接口,则在序列化子类时,子类的属性状态会被写入而父类的属性状态将不被写入。所以如果想要父类属性状态也一起参与序列化,就要让它也实现Serializable接口。
另外,如果父类未实现Serializable接口则反序列化生成的对象会再次调用父类的构造函数,以此完成对父类的初始化。所以父类属性初始值一般都是类型的默认值。比如下面的代码,Father类的属性不会参与序列化,反序列化时Father对象的属性的值为默认值0。
哪些字段会被序列化
在序列化时对象的哪些字段会参与到序列化中呢?其实有两种方式决定哪些字段会被序列化。一是默认方式,Java对象中的非静态和非transient的字段都会被定义为需要序列化的字段。另外一种方式是通过ObjectStreamField数组来声明需要序列化的对象。
可以看到对象中普通的属性都是默认会被序列化的,而如果某些包含了敏感信息的属性我们不希望它参与序列化,那么最简单的方式就是可以将该字段声明为transient。
如何使用ObjectStreamField呢?举个例子,如下代码中A类中有name和password两个字段,我们通过ObjectStreamField数组声明只需序列化name字段。我们不必纠结为什么这样声明,这仅仅是一个约定而已。
Externalizable接口
Externalizable接口主要就是提供给用户自己控制序列化内容,虽然前面我们也看到了transient和ObjectStreamField都能定义参与序列化的字段,但实际上Externalizable接口提供了更加灵活的方式。可以看到它其实继承了Serializable接口,然后提供了writeExternal和readExternal两个方法,我们就是在这两个方法内控制序列化和反序列化。
比如下面的例子,我们可以在writeExternal方法中额外写入Date对象,然后再写入value值。对应地,反序列化时则是在readExternal方法中读取Date对象和value。这样就完成了自定义序列化操作。
写入时代替
正常情况下序列化某个对象时写入的正是当前的对象,但如果说我们要替换当前的对象而写入其他对象的话则可以通过writeReplace方法来实现。比如下面,person类通过writeReplace方法最终可以写入Object数组对象。所以我们在反序列化时就不再是转换成Person类型,而是要转换为Object数组对象。
读取时代替
上面介绍了在写入时可以替换对象,而在读取时也同样支持替换对象的,它是通过readResolve方法实现的。比如下面的代码,在readResolve方法返回2222,则反序列化读取时不再是Person对象,而是2222。
AQS序列化
JDK内置并发AQS同步器实现了Serializable接口,其中head和tail变量声明为transient。也就是说如果对AQS同步器对象进行序列化的话,队列是不参与序列化的,只有同步状态会参与序列化。也就是说序列化会让AQS丢失队列信息,只能保留同步状态信息。
本文分享自微信公众号 - 码农架构(iByteCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。