Java自带Collection、Guava等类库,提供了便捷的Set、Map、List等常用数据结构。在日常开发中,这些数据结构已经满足我们的需求。这些数据结构是基于泛型实现的,提供了一个“架子”,用户可以自行设定里面的具体类型。但涉及到原始类型Primitive types(long、int、double…)作为数据结构中的元素时,因为Java泛型的限制,对这些数据结构的操作会带来很多“不必要的”原始类型的装箱和拆箱。
例如:
1 | Map<Long, Long> map = ... |
相当于:
1 | Map<Long, Long> map = ... |
为了这些问题,出现了很多支持原生类型的集合类库例如Trove、Eclipse Collections、koloboke、fastutil和argona。
具体的性能测试对比参见 https://github.com/austinv11/Long-Map-Benchmarks
从性能测试上来看Koloboke综合表现最好。但介于Koloboke在2016年后停滞在了1.0.0版本,用在生产环境还是有点虚的。论稳定性和功能的完整性,Eclipse Collections更胜一筹。
Koloboke
下面通过long-long map来初窥一下Koloboke。
使用
HashLongLongMap
除了实现java.util.Map<Long, Long>
的所有接口以外,还提供了long get(long key)
和long put(long key, long value)
等支持原生类型操作的接口。让我们可以以较低的改造成本将HashMap换成Koloboke的对应实现。
注意:
- 不支持
null
key、null
value。 - 不支持多线程,但是是fail fast的。
- 不支持
Cloneable
、Serializable
。 - 不能保证维持多于250,000,000个元素。当大小超过限制时,会抛出
HashOverflowException
。
选择
koloboke实现类库需要针对每种原始类型编写相应的实现,因此类库包会比较大(19MB)(但是方便呀)。koloboke除了提供实现类库以外,还提供了Compile,用户可以根据自己的需要生成所需的类。除了按需生成,Compile生成的实现在某些特殊场景上还比koloboke实现类库快5%。
实现
HashLongLongMap
的实现类使用的实现方式和Java里面的HashMap
不同,在解决Hash冲突时,使用的是开放地址法。如下图所示,它的key和value紧密相连。key存在偶数位,value存在相邻的奇数位。
优点:
- 当Hash冲突时,不需要拉链,减少在添加和删除元素时的拉链的Node创建和销毁。
- key和value在内存上紧密相连,(臆想)在同一缓存行,数据存取会比较快。
- 存储使用long[]数组,并提供原生类型操作接口,避免不必要的装箱拆箱。
缺点: - 当Hash冲突时,需要另外寻找位点,插入和查询性能会降低。(其实这对于拉链法也是一样的,Hash冲突,需要拉链,查询性能会降低)。
查找、插入和删除操作首先是找到key所在的位点index。index的计算方式是:
1 | (mask(key)) & (capacityMask = (tab.length) - 2) |
当index所在位置没有数据,即没有冲突时,就可以将tab[index] = key; tab[index + 1] = value;
。如果冲突就设置index = (index - 2) & capacityMask
。
那么问题来了怎么判断是否有冲突呢?存入的key是啥都有可能呐,又不能设置为null
表示没有设置过值。为了解决这个问题,这里很鸡贼的引入了一个freeValue的概念。正如freeValue这个名字,freeValue就是不存在Map的keys里面的一个自由自在的long值。
那么问题又来了,万一我插入的key的值和freeValue相等呢?那我能怎么办,只能换一个不存在的freeValue值咯。(并且把数组里面原来的老的freeValue换成新的freeValue)。
增和查知道了,那删就是直接把key所在的index设为freeValue,标记为没有数据就行了吧(并不)。之前其他的数据因为冲突可能找了好几次才找到空位。这里直接设置为freeValue,那这个位点前面的数据可能就找不到了。所以需要根据情况进行相应的数据移动。如下图所示,KEY2因为KEY1已经占据,所以到前面找了个位置。当KEY1删掉后,KEY2也要进行相应的移动,移动到KEY1的位置。
删除移动的逻辑(简化版)
1 | indexToRemove = indexToShift = index |
Eclipse Collections
现有的原生类型类可能会出现OOM的BUG,详见 ISSUE 。预计在 10.0 milestone version修复
使用
LongLongHashMap
只提供了操作原生类型的接口,没有实现java.util.Map<Long, Long>
,在改造的时候可能工作量较大。
实现
Eclipse Collections的LongLongHashMap
与前者的数据存储格式类似,都是KEY、VALUE紧密相邻存在一个数组中。不同的是它分别用key=0和key=1来表示空单元和被删除的单元。查找和插入的逻辑和Koloboke的HashLongLongMap
类似,先找index,key冲突的话就去下一个位置找,匹配后key的index+1就是value存储的位置。
那么问题来了,那我要插入的数据的key为0或1怎么办(问题咋这么多)?LongLongHashMap
使用了两个哨兵对象(sentinel object)来存储当key为0或1的值。查找、插入和删除都对这两个哨兵对象操作。
删除逻辑相对与Koloboke就简单很多,因为被删除的单元的key用1来表示,不需要在删除单元后移动其他的单元。只需要将被删除单元的key设置为1、value设置为0。
扩展阅读:
性能测试
测试程序
1 | (Scope.Benchmark) |
1 | PutDeleteTest.concurrentHashLongLongMapTest thrpt 5 25222540.523 ± 2219795.461 ops/s |
更为详细的测试,分析,待续。。。