Java集合框架和各实现类性能测试

Java语言集合框架提供一系列集合接口类 (collection interface)和实现类,满足对集合中元素对象的各种集合抽象操作。

1. 集合接口类Collection/List/Set/Map

下图显示了在包java.util.*中主要的核心集合接口类,

上图中的Collection/List/Set/Map等都是泛型接口,各自的定义和作用如下:

  • Collection是集合的顶层接口定义,其它的集合类都继承于Collection(除Map),这个接口定义了对集合元素的增删改查,及其提供interator用于循环整个集合中的元素列表等等。
  • Set是一个不能有重复元素的集合。
  • List是一个有序元素集合,有序是指按照加入/插入数组位置的顺序,集合中可以有重复的元素,可以认为List就是一个数组,访问时可以通过位置index直接访问元素。
  • Map是对键值对(key/value)元素的集合,集合中不能有重复的key。
  • Queue/Deque是一个提供能够进行队列操作的集合,比如FIFO(先进先出)/ LIFO(后进先出)。
  • SortedSet/SortedMap是一个按照元素进行升序排列的集合,集合中的元素排序取决于Comparator的实现。

2. 集合实现类和工具类

集合的实现类很多,JDK中提供的实现类都在java.util.*包中,其中List/Set/Map有如下几个实现类,

  • List
    • ArrayList/LinkedList/Vector
  • Map
    • HashMap/LinkedHashMap/TreeMap/WeakHashMap
    • Hashtable/ConcurrentHashMap
  • Set
    • HashSet/LinkedHashSet/TreeSet

集合的工具类Collections/Arrays提供一些集合的复制、比较、排序和转换操作,

  • Utilities
    • Collections/Arrays

上述各个实现类和接口类的关系见下图,

3. 集合实现类的特点和数据结构

下面以表格形式列出各个实现类的特点和区别,方便进行对比,其中不同的数据结构决定了各个集合实现类的不同性能特点,详细的数据结构描述见后面注解。

集合接口 集合实现类 是否按插入顺序存放 是否按有序存放(1) 是否可以存放重复元素 是否线程安全 数据结构特性描述
List ArrayList Yes No Yes No 基于动态数组的数据结构,注2
LinkedList Yes No Yes No 基于双向链表的数据结构,查询慢,插入快,注3。
Vector Yes No Yes Yes* Deprecated,注4。
Map HashMap No No Yes No 基于哈希表的元素数据离散分布,注5。
LinkedHashMap No No Yes No 基于哈希表的元素数据离散分布,除此之外集合元素数据之间有双向链表指针,注6。
TreeMap No Yes Yes No 基于红黑树的数据结构,元素需要提供Comparator实现,用于有序存放,注7。
WeakHashMap No No Yes No
Hashtable No No Yes Yes 基于哈希表的元素数据分散分布,通过对象锁实现线程安全
ConcurrentHashMap No No Yes Yes 通过lock实现线程安全,在多线程环境中比Hashtable有更好的并发性能
Set HashSet No No No No 底层使用HashMap实现
LinkedHashSet Yes No No No 底层使用LinkedHashMap实现
TreeSet No Yes No No 底层使用TreeMap实现,元素需要提供Comparator实现,用于有序存放

注1:元素是否按有序存放,是指集合中元素根据Comparator进行比较后升序排列。

注2:ArrayList是基于动态数组的数据结构,数据插入时,会导致整个后端数据的往后移动一位,所以插入速度慢,但是根据位置索引可以直接访问元素数据,所以通过位置索引查询数据速度会很快。

注3:LinkedList是基于双向链表的数据结构,插入快,但是查询会比较慢。另外LinkedList支持getFirst/getLast/removeFirst/removeLast/addFirst/addLast操作,可以方便实现FIFO/LIFO队列操作。双向链表的数据结构如下图所示,

注4:Vector由于其在所有的get/set上进行了synchronize,导致难于在并发编程发挥作用,在很多时候可以使用List list = Collections.synchronizedList(new ArrayList())方法取代,目前不建议使用Vector来用于线程安全的编程。

注5:HashMap基于哈希表的元素数据离散分布,是指数据按照一定规则进行离散,比如数值按16取模,各自落入不同的子集合,因此数据元素排列插入后无序,见下图所示,

注6:LinkedHashMap在集合元素数据之间有双向链表指针,数据的删除和插入快,数据元素排列后无序,见下图所示,

注7:TreeMap基于红黑树(近平衡二叉树)的数据结构,这个数据结构最大的特点是各个元素数据达到平衡分布,最远和最近叶子节点离根节点距离相差不超1,使得元素的查找和插入速度达到O(log N)级别。

4. 集合实现类的插入操作

我们可以尝试为各个集合实现类进行插入数据操作,然后查看数据元素在集合中的数据排列,下面主要观察数据排列是否有序,是否按照插入顺序排列。通过观察,可以更加深入地了解各个实现类的数据结构和特性。

List的数据插入

下面的代码新建了三个List实现类实例,然后依次插入10个随机数,最后打印出列表数据。

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new LinkedList<>();
List<Integer> list3 = new Vector<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入List中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 list1.add(value);
 list2.add(value);
 list3.add(value);
}

App.logMessage("ArrayList:" + list1);
App.logMessage("LinkedList:" + list2);
App.logMessage("Vector:" + list3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:41
第 1 次产生的随机数为:68
第 2 次产生的随机数为:62
第 3 次产生的随机数为:4
第 4 次产生的随机数为:18
第 5 次产生的随机数为:38
第 6 次产生的随机数为:97
第 7 次产生的随机数为:9
第 8 次产生的随机数为:19
第 9 次产生的随机数为:1
ArrayList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
LinkedList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
Vector:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]

可以看到,各个List的数据元素排列和插入顺序一致,这也是由于动态数组的数据结构带来的特性。

Set的数据插入

下面的代码新建了三个Set实现类实例,然后依次插入10个随机数,最后打印出列表数据。

Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new LinkedHashSet<>();
Set<Integer> set3 = new TreeSet<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入Set中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 set1.add(value);
 set2.add(value);
 set3.add(value);
}

App.logMessage("HashSet:" + set1);
App.logMessage("LinkedHashSet:" + set2);
App.logMessage("TreeSet :" + set3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:51
第 1 次产生的随机数为:86
第 2 次产生的随机数为:24
第 3 次产生的随机数为:66
第 4 次产生的随机数为:76
第 5 次产生的随机数为:59
第 6 次产生的随机数为:13
第 7 次产生的随机数为:34
第 8 次产生的随机数为:89
第 9 次产生的随机数为:21
HashSet:[66, 34, 51, 21, 86, 24, 89, 59, 76, 13]
LinkedHashSet:[51, 86, 24, 66, 76, 59, 13, 34, 89, 21]
TreeSet :[13, 21, 24, 34, 51, 59, 66, 76, 86, 89]

可以看到,HashSet/LinkedHashSet无序,TreeSet按大小依次排列。这是由于HashSet/LinkedHashSet/TreeSet底层实现用的是HashMap/LinkedHashMap/TreeMap,因此继承了各自的特点。

Map的数据插入

下面的代码新建了五个Map实现类实例,然后依次插入10个随机数(随机数为key),最后打印出列表数据。

Map map1 = new HashMap();
Map map2 = new TreeMap();
Map map3 = new LinkedHashMap();
Map map4 = new WeakHashMap();
Map map5 = new Hashtable();
Map map6 = new ConcurrentHashMap();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入map中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 if (!map1.containsKey(value)){
 map1.put(value, i);
 map2.put(value, i);
 map3.put(value, i);
 map4.put(value, i);
 map5.put(value, i);
 map6.put(value, i);
 }
}

App.logMessage("产生的随机数为key,index为value");
App.logMessage("HashMap:" + map1);
App.logMessage("TreeMap:" + map2);
App.logMessage("LinkedHashMap:" + map3);
App.logMessage("WeakHashMap:" + map4);
App.logMessage("Hashtable:" + map5);
App.logMessage("ConcurrentHashMap:" + map5);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:48
第 1 次产生的随机数为:86
第 2 次产生的随机数为:81
第 3 次产生的随机数为:19
第 4 次产生的随机数为:90
第 5 次产生的随机数为:74
第 6 次产生的随机数为:55
第 7 次产生的随机数为:29
第 8 次产生的随机数为:89
第 9 次产生的随机数为:65
产生的随机数为key,index为value
HashMap:{48=0, 81=2, 65=9, 19=3, 86=1, 55=6, 89=8, 90=4, 74=5, 29=7}
TreeMap:{19=3, 29=7, 48=0, 55=6, 65=9, 74=5, 81=2, 86=1, 89=8, 90=4}
LinkedHashMap:{48=0, 86=1, 81=2, 19=3, 90=4, 74=5, 55=6, 29=7, 89=8, 65=9}
WeakHashMap:{90=4, 74=5, 89=8, 29=7, 65=9, 55=6, 81=2, 86=1, 48=0, 19=3}
Hashtable:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}
ConcurrentHashMap:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}

可以看到,TreeMap按key大小升序排列,LinkedHashMap按value大小升序排列,其它无序。

集合实现类的性能比较

为了比较各个集合实现类的性能,对各个集合实现类进行如下三个操作,

  • 插入:在集合中插入0-N个数据元素,N为指定性能测试要达到的数组大小
  • 查询:在集合中一一查询0-N个数据元素,N为指定性能测试的数组大小,换句话说轮询集合中所有元素
  • 删除:在集合中一一删除0-N个数据元素,N为指定性能测试的数组大小,换句话说删除集合中所有元素

测试数组大小分别为1万、5万、10万、15万,线性倍增,然后观察各个集合实现类在不同数组大小下的性能表现。

注:在Map中查询分别添加了通过key/value查询的测试。

测试环境:Java版本为1.8.0_111,主机环境:Win7 SP1 x64, intel i5-6600K 4 cores 3.5GHz CPU, 16G memory。

可以看到,

  • List集合实现类在插入、查询、删除操作上,随着数组变大有明显的性能开销增加,性能开销的增加倍数超过数组大小的增加倍数。
  • Map集合实现类在通过value查询性能开销很大,在实际编程中,尽量避免此类操作
  • 表中有些操作时间开销都在10毫秒内,随着数组倍增,性能表现不错,这些操作可以在编程中多加利用。

测试程序代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

上述测试程序代码样例在文件路径171117_java_collection\demo中。

参考资料

Service Mesh介绍

微服务架构发展到今天,基本组件包括服务注册和发现、服务通信和治理、故障熔断恢复、配置、安全、监控等等,这些微服务组件和功能在业界已经深入人心,在各大互联网公司开花结果,业界知名开源的微服务框架有Netflix OSS、Spring Cloud及其国内的阿里Dubbo等等,各种框架百花齐放。但微服务框架自身组件繁多,本身的治理问题亟待解决。

1. 什么是Service Mesh?

Service Mesh是一个基础设施层,其独立运行在应用服务之外,提供应用服务之间安全、可靠、高效的通信,并为服务通信实现了微服务运行所需的基本组件功能,包括服务注册发现、负载均衡、故障恢复、监控、权限控制等等。Service Mesh的中文译为“服务网格”。

一个典型的Service Mesh部署网络结构图如下,

其中绿色方块为应用服务,蓝色方块为Sidecar Proxy,应用服务之间通过Sidecar Proxy进行通信,整个服务通信形成图中的蓝色网络连线,图中所有蓝色部分就形成了Service Mesh。

2. 为什么需要Service Mesh

最主要的理由来自于,Service Mesh在提供微服务框架功能的同时,它是一个独立运行在应用服务之外的模块,这带来的好处就是,应用服务不再需要为接入微服务框架而在代码和配置中添加繁多的依赖库和依赖配置项,实现了微服务框架和应用服务之间的解耦,让应用服务代码更加纯粹地去实现自己的业务逻辑,能够更加轻松地接入微服务框架,享受微服务框架带来的各种服务治理功能。

目前业界知名的微服务框架,比如Netflix OSS、Spring Cloud及其国内的阿里Dubbo等等,都在特定组件、特定语言上有偏向的支持,开发者在项目的开发过程中,都要在项目代码中和各个组件打交道,添加各种“胶水”代码。

请见如下一个Spring Boot项目,它通过注解方式打开各个组件开关,

@SpringBootApplication
@EnableConfigServer
@EnableEurekaServer
@EnableEurekaClient
@EnableHystrix
@EnableHystrixDashboard
@EnableTurbine
@EnableDiscoveryClient
@EnableFeignClients
@EnableZuulProxy
@EnableZuulServer
Public class Application {
   SpringApplication.run (Application.class, args);
}

可以看到,即使开发一个简单的项目功能,各个组件依赖库一个都不能少,而这些问题正是Service Mesh所要解决的。

3. Service Mesh的发展历程

Netflix OSS在微服务架构上有很早的探索,其开源出很多微服务框架所需的组件,但这些组件库主要使用的是Java开发语言,为了方便非Java语言的应用服务也能够对接这些组件,在2014年Netflix OSS开源了一款Prana的产品,其主要功能有,

  • 通过Eureka组件实现应用服务的注册和发现
  • 健康检查
  • 通过Ribbon组件实现应用服务的负载均衡
  • 通过Archaius组件实现应用服务的配置管理

这个Prana产品是一个基于RxNetty的HTTP代理服务,随应用服务一起部署,这个Prana已经有了Service Mesh中Sidecar功能原型。

当前两个比较重要的service mesh项目分别为Linkerd和Envoy Proxy,其中Linkerd在2016年1月发布了0.0.7公开版本,而Envoy Proxy在2016年9月发布它们的第一个版本v1.0.0,两者都提供HTTP/gRPC/Thrift的服务通信方式。

如下是Linkerd的功能图解,

Envoy Proxy的图解请见后续的Istio介绍。

在2017年云原生计算基金会(CNCF,Cloud Native Computing Foundation)分别在1月和9月先后接受了Linkerd和Envoy Proxy项目,正式由CNCF来负责维护开发,这也宣布了业界对Service Mesh的支持和拥抱。

4. Servcie Mesh的整体集成解决方案Istio

Service Mesh主要解决的是微服务之间的网络通信交互,随着业务服务增加,整个Service Mesh会变得庞大和复杂之后,这个时候需要对Service Mesh的管理功能进行抽象出来,从而满足丰富的微服务运营需求,而Istio就是提供这样一个整体集成解决方案的项目。

Istio由Google、IBM、Lyft Envoy联手开发,一开始就定位于实现Service Mesh模式的微服务框架,在2017年5月发布0.1版本,然后在10月发布了0.2版本。

一个Istio Service Mesh在逻辑上可以分成两大区块,

  • 数据区(data plane):由通信代理组件(Envoy/Linkerd等)和组件之间的网络通信组成。
  • 控制区(control plane):负责对通信代理组件进行管理和配置

Istio的架构图如下,

在图中可以看到Istio Service Mesh的两大区块。

架构图中各个子模块功能如下,

  • Envoy:一个使用C++语言开发的高性能通信代理,负责各个应用服务之间通信
  • Pilot:管理和配置Envoy,提供服务发现、负载均衡和智能路由,保证弹性服务(服务超时次数、重试、熔断策略)。
  • Mixer:信息监控检查
  • Istio-Auth:提供服务和服务、用户和服务之间的认证服务,实现访问控制,解决是谁访问的是哪个API的问题

其中,图中的通信代理组件为Envoy,这是Istio原生引入的,但Linkerd也能够集成对接Istio。

5. 参考资料

Mesos架构和工作流程简介

1. 什么是Mesos

Mesos是Apache软件基金会维护的一个开源软件,它负责管理一批服务器集群,并将所有服务器集群的CPU、GPU、内存、存储、端口和其它相关计算资源进行了抽象统一,让用户进行动态配置和使用,提高整个系统的资源利用率,并以集群分布式的方式保证系统运行的高可用和弹性扩展。

2. Mesos架构和工作流程

官网中有个Mesos的架构图如下,

主要的模块有,

  • Mesos Master:管理所有机器资源信息,一般会部署多台来保证master的高可用,以resource offers的方式定时告知scheduler可用资源。
  • ZooKeeper:实现当前工作Mesos Master的选举,并实现数据一致性。
  • Mesos Agent:集群中每个机器都会部署一台Mesos Agent,定时汇报当前机器可用资源给Master,并从Master获取分配的任务信息,调用Executor来运行。
  • Scheduler:定时得到Master发送过来的resource offers,将任务列表中各个任务的资源需求进行一一匹配,一旦资源需求获得满足,则告诉Master使用匹配到的资源启动任务。
  • Executor:被Agent调用,根据指定的任务和资源信息,执行任务。

其中Scheduler和Executor是开发者根据自己需要进行开发,业界里各种Mesos Framework也就是实现这两部分,例如图中的Hadoop Mesos/MPI Mesos,还有Mesosphere公司的marathon,HudSpot公司的Singularity,国内数人云的Swan等。

Mesos提供两层调度,一层是Agent调用Executor执行Master分配的任务,另外一层是任务的资源匹配和调度放到Scheduler,由Scheduler推送任务到Master。由于Scheduler和Executor的可动态调整,这样也使得任务调度策略和执行变得可动态配置。

整个Mesos系统的主要工作流程如下,

  1. Mesos Agent定期上报各个机器的资源(CPU、Memory、磁盘、端口号)
  2. Mesos Master收集所有Agent可用的资源,定期推送resource offer给Scheduler,offer中描述了可用的资源信息。
  3. Scheduler启动时,会找到Mesos Master并注册,定期获取Master推送给的resource offer,以此了解可用的资源。
  4. 用户向Scheduler申请任务需要的资源,执行相关任务,例如申请2CPU 4G Mem来运行程序Demo。
  5. Scheduler在获取到Master推送的offer后,当offer中的资源满足用户申请的任务需求,就向Master申请执行。
  6. Mesos master根据Scheduler申请,在相应的资源上调用Agent执行任务。

整个流程图见如下,

需要注意的是,Mesos的四大组件(Master/Slave/Scheduler/Executor)之间的通信,是通过libprocess实现actor model模型的进程间消息异步通信,每个进程是一个actor。

见上图,在Mesos的master节点中,每个Framework以及Slave都是一个远程的actor。而slave节点上,每个executor是一个actor,只不过内置的executor是在同一个进程中的,而其他自定义的executor是独立的进程,executor和slave之间通过进程间通信方式(网络端口)交互。

Actor模型通信带来的好处是省去了对消息队列的依赖,但同时由于消息都是异步的,需要actor处理消息的丢失以及超时逻辑,Mesos无法保证消息的可靠投递,提供的投递策略是 at-most-once(至多一次,不会重试)。

3. 如何开发一个Mesos Framework

Mesos提供支持多种语言的Framework开发,包括Java/Python/Scala等,下面以Java语言为例介绍Mesos Framework的开发,其它语言类似。

一个Mesos Framework主要实现两个模块,

  1. Scheduler
  2. Executor

其中Scheduler会是单独可运行的项目程序,比如一个Java程序或者一个Java Web后端服务;Executor则会是Jar包项目,打出Jar包后由Mesos Agent引用并启动。

在两个项目中各自引入依赖包,

<dependency>
<groupId>org.apache.mesos</groupId>
<artifactId>mesos</artifactId>
</dependency>

在包org.apache.mesos中提供如下主要抽象接口,

  • Scheduler:这是Framework要实现的Scheduler接口,用于Mesos Master回调。
  • Executor:这是Framework要实现的Executor接口,用于Agent回调。
  • SchedulerDriver:这是Scheduler和Mesos进行通信的抽象接口。
  • ExecutorDriver:这是Executor和Mesos进行通信的抽象接口。

在两个项目中需实现上述的Scheduler和Executor抽象接口,各个接口方法的实现需求请参见Apache Mesos项目源代码中对其的接口描述,可以通过如下git命令下载代码,
git clone git@github.com:apache/mesos.git

在Apache Mesos项目代码中有对Mesos Framework开发提供的样例实现,在其文件目录中,

  • mesos\src\examples\java\TestFramework.java
  • mesos\src\examples\java\TestExecutor.java

前一个实现了org.apache.mesos.Scheduler,是一个可单独运行的Java程序;后一个实现了org.apache.mesos.Executor,是一个Jar包。

注:在包org.apache.mesos中还有两个抽象接口:SchedulerDriver和ExecutorDriver,Apache Mesos提供两个Driver的默认具体实现MesosSchedulerDriver和MesosExecutorDriver,所以Mesos Framework可以直接使用,用于和Mesos Master通信,不用自己实现这两个Driver。

4. 参考资料