Java 中 BitSet

本文详细介绍了Java中的BitSet数据结构,用于高效地表示大量数据的存在性。通过位运算,BitSet可以极大地节省内存,尤其在处理大量URL或字符串时。文章讨论了BitSet的工作原理,包括如何避免位运算冲突,以及如何通过多个哈希算法缓解哈希冲突。此外,还提出了BloomFilter作为进一步优化的选择,以减少误判率。最后,强调了合理设置BitSet值域以防止内存溢出的重要性。
摘要由CSDN通过智能技术生成

Java 中 BitSet 就是“位图”数据结构,根据“位图”的语义,数据的存在性可以使用 bit 位上的1或0来表示;一个 bit 具有2个值:0和1,正好可以用来表示 false 和 true。对于判断“数据是否存在”的场景,我们通常使用 HashMap 来存储,不过 hashmap 这个数据结构 Key 和 Value 的保存需要消耗较多的内存,不适合保存较多的数据,即大数据场景;比如在有10亿条 URL 中判定一个“www.baidu.com/a” 是否存在,如果我们使用常规的 hashmap 来保存将是不现实的,因为 URL 本身需要占据较多的内存而无法直接操作。如果我们使用 bitset 来保存,那么可以对一条 URL 求 hashcode,并将数字映射在 bitset 上,那么事实上它只需要bitset 上的一个 bit 位即可,即我们1位空间即可表达一个 URL 字符串的存在性。

所谓“存在性”,就是通过 BitSet 来检测一个数字是否存在。

BitSet原理

Java 中,一个 long 型数字占用64位空间,根据上述“位图”的概念,那么一个 long 型数字(4个字节)就可以保存64个数字的“存在性”状态(无碰撞冲突时,即 true、false 状态)。比如50个数字{0,1,10,…63},判定“15”是否存在,那么我们通常会首先将这些数字使用数组或者 hashmap 保存,然后再去判定,那么保存这些这些数据需要占用64 * 64位;如果使用位图,那么一个long型数字即可。(如果换成50个字符串,那么其节约的空间可能更大)。

  1. BitSet 只面向数字比较,比如 set(int a,boolean value) 方法,将数字 a 在 bitSet 中设定为 true 或者 false;此后可以通过 get(int a) 方法检测结果。对于string类型的数据,如果像使用 BitSet,那么可以将其 hashcode 值映射在 bitset 中。

  2. 首先我们需要知道:1,1<<64,1<<128,1<<192…等,这些数字的计算结果是相等的(位运算);这也是一个long数字,只能表达连续的(或者无冲突的)64个数字的状态,即如果把数字1在long中用位表示,那么数字64将无法通过同一个long数字中位表示–冲突;BitSet内部,是一个long[]数组,数组的大小由bitSet接收的最大数字决定,这个数组将数字分段表示[0,63],[64,127],[128,191]…。即long[0]用来存储[0,63]这个范围的数字的“存在性”,long[1]用来存储[64,127],依次轮推,这样就避免了位运算导致的冲突。

|------------|----------|----------|----------|----------|  
|  
|  数字范围      [0,63]     [64,127]  [128,191]     ...   |  
|------------|----------|----------|----------|----------|  
|  
|long数组索引      0           1          2         ...   |  
|------------|----------|----------|----------|----------|  
  1. bitSet 内部的 long[] 数组是基于向量的,即随着 set 的最大数字而动态扩展。数组的最大长度计算:
 (maxValue - 1) >> 6  + 1  
  1. BitSet 中 set 方法伪代码:
public void set(int number) {  
    int index = number >> 6;//找到number需要映射的数组的index。  
    if(index + 1 > length) {  
        ensureCapacity(index + 1);//重新扩展long[]数组  
    }   
    long[index] |= (1L << number);//冲突解决  
}  

使用 BitSet:

本例中使用 bitSet 做 String 字符串的存在性校验。

BitSet bitSet = new BitSet(Integer.MAX_VALUE);//hashcode的值域  
  
//0x7FFFFFFF  
String url = "http://baidu.com/a";  
int hashcode = url.hashCode() & 0x7FFFFFFF;  
bitSet.set(hashcode);  
  
System.out.println(bitSet.cardinality());//着色位的个数  
System.out.println(bitSet.get(hashcode));//检测存在性  
bitSet.clear(hashcode);//清除位数据  

BitSet 与 Hashcode 冲突

因为 BitSet API 只能接收 int 型的数字,即只能判定 int 数字是否在 bitSet 中存在。所以,对于 String 类型,我们通常使用它的hashcode,但这有一种隐患,java中hashcode存在冲突问题,即不同的String可能得到的hashcode是一样的(即使不重写hashcode方法),如果我们不能很好的解决这个问题,那么就会出现“数据抖动”—不同的hashcode算法、运行环境、bitSet容量,会导致判断的结果有所不同。比如A、B连个字符串,它们的hashcode一样,如果A在BitSet中“着色”(值为true),那么检测B是否在BitSet存在时,也会得到true。

这个问题该如何解决或者缓解呢?

1)调整hashcode生成算法:我们可以对一个String使用多个hashcode算法,生成多个hashcode,然后在同一个BitSet进行多次“着色”,在判断存在性时,只有所有的着色位为true时,才判定成功。

String url = "http://baidu.com/a";  
int hashcode1 = url.hashCode() & 0x7FFFFFFF;  
bitSet.set(hashcode1);  
  
int hashcode2 = (url + "-seed-").hashCode() & 0x7FFFFFFF;  
bitSet.set(hashcode2);  
System.out.println(bitSet.get(hashcode1) && bitSet.get(hashcode2));  
//也可以在两个不同的bitSet上进行2次“着色”,这样冲突性更小。但会消耗双倍的内存  

其实我们能够看出,这种方式降低了误判的概率。但是如果 BitSet 中存储了较多的数字,那么互相覆盖着色,最终数据冲突的可能性会逐渐增加,最终仍然有一定概率的判断失误。所以在 hashcode 算法的个数与实际 String 的个数之间有一个权衡,我们建议: “hashcode算法个数 * String字符串的个数” < Integer.MAX_VALUE * 0.8

  1. 多个BitSet并行保存:改良1)中的实现方式,我们仍然使用多个 hashcode 生成算法,但是每个算法生成的值在不同的 BitSet 中着色,这样可以保持每个 BitSet 的稀疏度(降低冲突的几率)。在实际结果上,比1)的误判率更低,但是它需要额外的占用更多的内存,毕竟每个 BitSet 都需要占用内存。这种方式,通常是缩小 hashcode 的值域,避免内存过度消耗。
BitSet bitSet1 = new BitSet(Integer.MAX_VALUE);//127M  
BitSet bitSet2 = new BitSet(Integer.MAX_VALUE);  
  
String url = "http://baidu.com/a";  
int hashcode1 = url.hashCode() & 0x7FFFFFFF;  
bitSet1.set(hashcode1);  
  
int hashcode2 = (url + "-seed-").hashCode() & 0x7FFFFFFF;  
bitSet2.set(hashcode2);  
  
System.out.println(bitSet1.get(hashcode1) && bitSet2.get(hashcode2));  
  1. 是否有必要完全避免误判?
    如果做到100%的正确判断率,在原理上说BitSet是无法做的,BitSet 能够保证“如果判定结果为 false,那么数据一定是不存在;但是如果结果为 true,可能数据存在,也可能不存在(冲突覆盖)”,即“false == YEStrue == Maybe”。有人提出将冲突的数据保存在类似于 BTree 的额外数据结构中,事实上这种方式增加了设计的复杂度,而且最终仍然没有良好的解决内存占用较大的问题。

BloomFilter(布隆姆过滤器)

BloomFilter 的设计思想和 BitSet 有较大的相似性,目的也一致,它的核心思想也是使用多个 Hash 算法在一个“位图”结构上着色,最终提高“存在性”判断的效率。请参见 Guava BloomFilter。如下为代码样例:

Charset charset = Charset.forName("utf-8");  
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(charset),2<<21);//指定bloomFilter的容量  
String url = "www.baidu.com/a";  
bloomFilter.put(url);  
System.out.println(bloomFilter.mightContain(url));  

内存消耗

据上所述,BitSet 可以有效的降低内存的使用量,但是它的内存使用量是有内部 long 数组的大小决定,所以在创建 BitSet 时指定的值域非常重要,过大的值域将会导致 OOM(比如指定 Long.MAX_VALUE),在一个 BitMap 上存储 Integer.MAX_VALUE 个“着色”(注意,BitSet 只能对正数操作),大概消耗128M内存。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值