Java高并发与集合框架:JCF和JUC源码分析与实现
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

前言

1. 写在开篇

为什么撰写本书

笔者一直在信息系统建设的一线工作,承担过多种不同的工作职责。在工作中,笔者经常和掌握不同技术的朋友讨论具体问题的解决方案,发现在Java体系中,大家使用最多的是Java集合框架(JCF)和Java并发工具包(JUC)。实际上,JCF和JUC已经能够覆盖笔者及朋友们工作中遇到的超过8成的应用场景,但是大家往往无法快速匹配最合适的技术方案。此外,在JCF和JUC中存在大量可以在实际工作中借鉴的设计方案,虽然网络上有一些零散的关于集合的介绍,但深入讲解其工作原理的内容并不多,甚至有一些资料存在质量问题。

笔者“撸码”近17年,庆幸头未秃、智未衰。于是笔者在1年前产生了这样的想法,希望将自己在实际工作中梳理、总结的JCF、JUC的相关知识成体系地介绍给大家,也希望将自己在阅读JDK源码(包括JCF、JUC、I/O、NET等模块)后总结和思考的可用于实际工作的技术手段成体系地分享给大家。

有了想法,便着手行动,经过大半年的整理、撰写、调整,终成本书。因个人水平有限,书中难免有错误和疏漏之处,希望各位读者能诚意指出、不吝赐教。此外,各位读者可以直接通过笔者的邮箱yin-wen-jie@163.com联系笔者本人。

关于本书选择的JDK版本

由于JDK版本迭代速度较快,本书的整理和撰写又需要一个较长的时间,并且书中内容包括大量源码分析和讲解,因此本书首先要解决的问题,就是选定一个本书进行源码讲解和分析所依据的JDK版本。

这个问题需要从JDK版本的更新特点、大家使用JDK版本的习惯和撰写本书的目的来考虑,目前大家在实际工作中使用最多的版本是JDK 1.8和JDK 11,并且出于对Oracle商业运作的考虑,JDK 11之后的升级版本已不再免费提供,因此大家在生产环境中一般使用Open JDK作为运行环境。同样出于对Oracle商业运作的考虑,JDK版本的发布周期固定为每半年发布一个大版本、每3年发布一个LTS/LMS版本(长期支持版本),截至2021年4月,JDK 16已经发布(JDK 16是一个短期过渡版本),紧接着2021年9月,JDK 17正式发布(这是一个LTS/LMS版本),这显然加大了读者学习和筛选JDK版本的工作量。

好消息是Oracle基本开源了所有已成熟的JDK版本,是否商业化运行并不会影响我们对这些JDK源码的学习(只要不用于商业用途),而且JDK版本的向下兼容性保证了读者在了解JDK的工作原理后,可以将其应用到自己正在使用的JDK版本上。此外,越新的JDK版本对关键数据结构、关键算法实现过程的优化越多,本书希望在讲解过程中,可以尽可能多地将这些有益的优化点介绍给读者。

在综合考虑各因素后,本书将JDK 14作为本书讲解源码依据的主要版本(在后续内容中,如果不特别说明,那么代码分析都是基于JDK 14进行的)。JDK 14是在整理、编写本书时发布的版本。在该版本中,与本书主要内容有关的数据结构、核心算法、设计方案和之前的版本基本保持稳定和兼容,便于读者在常用的JDK 1.8、JDK 11中找到对应的实现位置。本书介绍的JDK 14中的源码内容是完全开源的,读者可以在Oracle官方网站直接下载这些源码。

本书的目标读者

本书前半部分可以作为Java编程的入门学习内容,也可以作为初学者进行JCF部分知识查漏补缺的参考资料;本书后半部分对基础知识有一定的要求,适合有一些Java编程基础的程序员阅读。此外,本书可以作为程序员对JCF部分和JUC部分知识结构进行梳理的参考用书。

2. 本书约定

1)关于源码注释及代码格式。

本书中有大量基于JDK 14的源码片段。笔者会对这些源码片段逐段说明、逐条分析,读者不用担心无法读懂这些源码。此外,大部分章节在对源码进行分析后,会使用图文方式对源码中的重要知识点进行归纳和总结。

引用大量的源码会占用篇幅。为了尽可能节约纸张,本书中的示例代码没有遵循Java官方推荐的注释规范和代码格式规范,本书会对代码和注释进行格式压缩。本书主要采用以下两种格式压缩方式。

• 采用单行注释代替多行注释。

Java官方推荐的多行注释方式采用的是“/***/”,如下所示。

这种注释方式非常清晰且容易阅读,但是占用了过长的篇幅,所以本书会将上述注释转换为单行注释,如下所示。

• 采用压缩格式替换单行代码块。

如果代码块中只有一行代码,那么Java允许省略代码块中的大括号“{}”。例如,在if语句的代码块中,如果只有一行执行代码,则可以采用如下方式进行书写。

但这种书写方式容易在排布紧凑的局部位置引起阅读障碍,所以针对源码中的这种简写方式,本书进行了简写还原和格式压缩。书中会恢复所有被简写的代码段落的大括号“{}”,从而方便对源码进行分析,并且将只有一行代码的代码块压缩成单行,如下所示。

2)关于JDK版本的命名。

JDK 1.2~JDK 1.8都采用1.X格式的小版本号,但是在JDK 1.8后,Oracle改为采用大版本号对其进行命名,如JDK 9、JDK 11等。本书也会采用这种命名方式,但是由于各个版本功能存在差异,因此为了表达从某个JDK版本开始支持某种功能或特性,本书会采用“+”符号表示。例如,如果要表达从JDK 1.8开始支持某种特性,则用JDK 1.8+表示;如果要表达从JDK 11开始支持某种特性,则用JDK 11+表示。

3)其他约定。

• 关于JVM的称呼约定。

本书无意深入分析JVM的内部运行原理,也不会深入讨论JVM每个模块负责的具体工作。例如,本书不会分析JIT(即时编译器)指令重排的细节,以及在什么情况下代码指令不会被编译执行,而会被解释执行。凡是涉及内部运行原理的内容,本书将其统称为JVM运行过程。此外,如果没有特别说明,那么本书提到的JVM都表示HotSpot版本的虚拟机。

• 关于方法的称呼约定。

由于Java中的方法涉及多态场景,因此本书需要保证对Java中方法的称呼不出现二义性。例如,java.lang.Object类中的wait()方法存在多态表达,代码如下。

在不产生二义性的情况下,本书会直接采用“wait()方法”的描述方式。如果需要介绍多态场景中方法名相同、入参不同的方法表达的不同工作特性,那么不加区别就会造成二义性,这时本书会采用“wait()”“wait(long)”分别进行特定描述。

• 关于图表的约定。

本书主要采用图文方式对Java源码进行说明、分析和总结,由于客观限制,大量的插图只能采用黑白方式呈现,因此如果有需要,则会在插图后的正文中或插图右上角给出图例说明。

• 关于System.out对象的使用。

在实际工作中,推荐使用slf4j-log4j方式进行日志/控制台输出,但本书中的代码片段大量使用System.out对象进行控制台输出,这并不影响读者理解这些代码片段的逻辑,也有利于不同知识水平的读者将精力集中在理解核心思路上。

• 关于包简写的约定。

本书大部分内容涉及Java集合框架(JCF)和Java并发工具包(JUC),JCF和JUC通常涉及较长的包路径。例如,在JUC中,封装后最终向程序员开放的原子性操作工具类位于java.util.concurrent.atomic包下。如果本书中每一个类的全称都携带这么长的包路径,那么显然是没必要的。为了节约篇幅,本书会使用包路径下每个路径点的首字母对包路径进行简写,如将“java.util.concurrent.atomic”包简写为j.u.c.atomic包。

• 关于集合、集合对象、队列的约定。

读者应该都已经知晓,对象是类的实例。本书将JCF中的具体类称为集合,将JCF中类的实例对象称为集合对象。队列是一种具有特定工作效果的集合,从继承结构上来说,本书会将JCF中实现了java.util.Queue接口的集合称为队列。这主要是为了表述方便,并不代表笔者认为集合、集合对象和队列在JVM工作原理层面上有任何差异。

3. 必要的前置知识

本书难度适中,但仍然需要读者对Java编程语言具备基本认知,这样才能通畅地阅读本书所有内容。这种基本认知与工作年限没有关系,属于只要是Java程序员就应该掌握的知识。

1)关于位运算的知识。

Java支持基于二进制的位运算操作。在Java中,使用“>>”表示无符号位的右移运算,使用“>>>”表示有符号位的右移运算,使用“<<”表示无符号位的左移运算,使用“<<<”表示有符号位的左移运算。

在Java中,基于整数的位运算相当于整数的乘法运算或除法运算。如“x>>1”表示将x除以2,“x<<1”表示将x乘以2。在JCF中,无论是哪个版本的Java源码,都会采用位运算来实现整数与2的乘法运算或除法运算。此外,读者需要知道如何对某个负整数进行二进制表达。在特定情况下,Java使用与运算替换取余运算。例如,通过语句“x&255”可实现取余运算,这句代码的意义为对256取余。

2)关于对象引用、引用传递和“相等”的知识。

Java中有八种基础类型和类类型,可以使用引用的方式给类的对象赋值。在调用方法时,除八种基础类型的变量外,传递的都是对象引用(包括基于基础类型的数组对象)。

Java中的“相等”有两种含义,一种是值相等,另一种是引用地址相等。值相等是由对象中的equals()方法和hashCode()方法配合实现的(如果两个对象的值相等,那么使用equals()方法对这两个对象进行比较会返回true,并且使用hashCode()方法对这两个对象进行比较返回的int类型的值也必然相同);引用地址相等是由两个对象(注意:不是基本类型数据)使用“==”运算符进行比较得到的。

本书虽然未涉及动态常量池、字符串常量池的相关知识,但需要读者知晓这些,否则无法理解类似于“String”的字符串对象或基础类型装箱后的对象关于“相等”的工作原理。

3)关于对象序列化和反序列化的知识。

Java的序列化和反序列化过程主要是指由java.io包支持的将对象转换为字节序列并输出的过程和将字节序列转换为对象并输入的过程。JCF中的大部分集合都对对象的序列化和反序列化过程进行了重新实现。其中要解决的问题有很多,包括提高各种集合在序列化和反序列化过程中的性能问题(这个很关键,因为集合中通常存储了大量数据),以及如何保证集合在不同JDK版本中进行反序列化时的兼容性问题。

本书不会专门讲解每一种集合在序列化和反序列化过程中的工作细节,以及如何解决上述问题,并且默认读者知晓Java中的对象可以使用writeObject(ObjectOutputStream)方法和readObject(ObjectInputStream)方法对序列化和反序列化过程进行干预。

4)关于线程的知识。

为了介绍在高并发场景中工作的集合,本书会先介绍Java并发工具包(JUC)中的相关知识点,所以读者需要知道Java中的基本线程使用方法,如如何创建和运行一个用户线程。

5)关于原子性操作的基础知识。

在阅读本书前,读者无须知道引起原子性操作问题的底层原因,但需要知道Java并不能保证所有场景中的原子性操作。例如,在多线程情况下,如果没有施加任何安全措施,那么Java无法保证类似于“i++”语句的原子性。

4. 本书的知识结构和脉络

由于JCF和JUC有非常庞大的知识体系,因此无法用有限的篇幅覆盖所有知识点。例如,本书并未讲解JCF中的每种集合对fail-fast机制的匹配设计,也没有讲解每种集合对对象序列化和反序列化的优化设计。此外,即使要讲解指定范围内的知识点,也需要有清晰的思路和知识脉络,从而帮助读者更好地理解。因此,在正式阅读本书内容前,需要了解本书内容的介绍路径。

首先,本书会介绍最基础的集合,它们都属于JCF的知识范畴,分别属于List、Queue、Map和Set性质的集合,并且与JUC不存在使用场景交集。在这一部分,本书会介绍这些集合的基本工作原理(这些集合的外在功能表现各不相同,但它们的内在数据结构具备共性),并且选择其中的重要集合及其数据结构来详细讲解。

然后,本书会向在高并发场景中工作的集合进行过渡,在这一部分,本书的内容难度有所提升,所以在正式介绍这些集合前,会先介绍与之有关的高并发知识。例如,在高并发场景中如何保证原子性、可见性和有序性,Java中两种管程的工作原理和使用方法,Java为什么需要通过自行实现的管程技术解决多线程问题。实际上,这些都是JUC的相关知识,如图0-1所示。

图0-1

最后,本书会介绍在高并发场景中使用的集合,这些集合主要负责两类任务,一类任务是在高并发场景中正确完成数据的存储工作并为多个线程分享数据,另一类任务是在高并发场景中主导消费者线程(从集合中读取数据的线程)和生产者线程(向集合中写入数据的线程)之间的数据传输工作。这部分主要介绍Queue/Deque集合,以及它们是如何在保证线程安全性的前提下,利用各种设计技巧提高工作效率的。

根据本书的知识逻辑,读者会从JCF部分的知识脉络过渡到JUC部分的知识脉络,最后回到JCF本身,其中涉及的集合和数据结构如图0-2所示。需要注意的是,本书不会对图0-2中的所有集合和数据结构进行详细讲解,而是有选择地进行详细讲解。例如,HashMap集合具有代表性,所以会对其进行详细讲解,然后在此基础上说明LinkedHashMap集合是如何对前者进行结构扩展的;对于HashSet集合,虽然该集合经常在工作中使用,但其工作原理依赖于HashMap集合,所以只会对其进行粗略讲解;为了让读者清晰理解那些适合工作在高并发场景中的集合是如何工作的,本书除了介绍Java中的两种管程技术、多线程中的三性问题等知识,还会详细介绍具有工作共性的数据结构(如堆、红黑树、数组、链表等),使读者能在高并发场景中结合数据结构进行思考和理解。

图0-2