1、BitSet原理
BitSet 是 Java 中用于处理位集(bit set)
的一种类,它允许以位的形式存储和操作布尔值
。下面是 BitSet 的底层原理介绍:
在JAVA中,一个Long占用8个字节(byte)、即64位(bit)
,一个long型数字就可以保存64个数字的“存在性”状态
(无碰撞冲突时)。比如50个数字{0,1,10,…63},判定“15”是否存在,那么我们通常会首先将这些数字使用数组保存,然后再去判定,那么保存这些这些数据需要占用64 * 64位;如果使用位图,那么一个long型8位即可
。(如果换成50个字符串,那么其节约的空间可能更大)。
对于string类型的数据,如果像使用BitSet,那么可以将其hashcode值映射在bitset中
首先我们需要知道: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 ... |
|------------|----------|----------|----------|----------|
- BitSet内部,是一个long[]数组,
数组的大小由bitSet接收的最大数字决定
。bitSet内部的long[]数组是基于向量的,即随着set的最大数字而动态扩展。数组的最大长度计算:举例最大数字如果是64,(64-1)/64+1=1
;最大数字如果是65,(65-1)/64+1=2
(maxValue - 1) >> 6 + 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所占
内存的大小
(最大数字/64+1)*8字节
,其中“最大数字/64+1”是计算出BizSet需要多少了long,一个long占8字节。 -
位操作
● BitSet 提供了多种操作方法来设置、清除和查询
特定位:
○ set(int bitIndex):设置指定的位为 1
。
○ clear(int bitIndex):将指定的位设置为 0
。
○ get(int bitIndex):获取指定的位的值
(0 或 1)。
○ flip(int bitIndex):切换(flip)指定的位的值。 -
位运算
● BitSet 支持一些常用的位运算,例如:
○ and():与操作,用于获取两个 BitSet 的交集。
○ or():或操作,用于获取两个 BitSet 的并集。
○ xor():异或操作,用于获取两个 BitSet 的差集。 -
性能
● 对于位的操作非常高效
,尤其是在存储大量布尔值时,因为它以压缩的形式存储
,节省了内存
。
● 大多数操作的时间复杂度是 O(1)
,但某些操作在处理非常大的集合时可能会涉及到数组的重新分配,影响性能
。 -
线程安全
● BitSet不是线程安全的
,因此在多线程环境中需要自行管理并发访问。 -
示例代码
import java.util.BitSet;
public class BitSetExample {
public static void main(String[] args) {
BitSet bitSet = new BitSet();
// 设置位
bitSet.set(0);
bitSet.set(2);
bitSet.set(3);
// 清除位
bitSet.clear(2);
// 获取位
System.out.println("Bit at index 2: " + bitSet.get(2)); // false
System.out.println("Bit at index 3: " + bitSet.get(3)); // true
// 与操作
BitSet anotherSet = new BitSet();
anotherSet.set(0);
anotherSet.set(1);
bitSet.and(anotherSet);
System.out.println("After AND operation: " + bitSet);
}
}
2、使用BitSet:本例中使用bitSet做String字符串的存在性校验。
//hashcode的值域
BitSet bitSet = new BitSet(Integer.MAX_VALUE);
//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);
3、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时,才判定成功。如字符串的int = hashcode % bitset的count
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中存储了较多的数字
,那么互相覆盖着色,最终数据冲突的可能性会逐渐增加,最终仍然有一定概率的判断失误
。所以在hashcode算法的个数与实际String的个数之间有一个权衡,我们建议: “hashcode算法个数 * String字符串的个数” < Integer.MAX_VALUE * 0.8。
2) 多个BitSet并行保存:
改良1)中的实现方式
,我们仍然使用多个hashcode生成算法
,但是每个算法生成的值在不同的BitSet中着色
,这样可以保持每个BitSet的稀疏度
(降低冲突的几率)。在实际结果上,比1)的误判率更低
,但是它需要额外的占用更多的内存
,毕竟每个BitSet都需要占用内存。这种方式,通常是缩小hashcode的值域,避免内存过度消耗
,如字符串的int = hashcode % bitset的count。
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));
3) 是否有必要完全避免误判?
如果做到100%的正确判断率,在原理上说BitSet是无法做的,BitSet能够保证“如果判定结果为false,那么数据一定是不存在
;但是如果结果为true,可能数据存在,也可能不存在
(冲突覆盖)”。有人提出将冲突的数据保存在类似于BTree的额外数据结构中,事实上这种方式增加了设计的复杂度,而且最终仍然没有良好的解决内存占用较大的问题。
4、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));
5、应用场景
内存占用率比较高,由于gc stop the world导致rt较长,通过分析发现某个对象的string集合所占内存较大、且string可以映射为一个固定的下标,那么可以用BitSet代替string集合,降低内存占用。封装通用的类:根据String集合构建BitSet、根据BitSet计算String集合。
public class CompressInfo<T> {
/**
* 原始对象集合
*/
private List<T> rawList;
/**
* 原始对象和集合下标的关系
*/
private Map<String, Integer> rawIndexMap;
/**
* 原始对象和集合下标的关系
*/
private Function<T, String> getKeyFunction;
/**
* 构造函数
*
* @param getKeyFunction 获取key的方法
*/
public CompressInfo(Function<T, String> getKeyFunction) {
this.rawList = Lists.newArrayList();
this.rawIndexMap = new HashMap<>();
this.getKeyFunction = getKeyFunction;
}
/**
* 添加原生数据
*
* @param data 原生数据
*/
public void add(T data) {
rawList.add(data);
rawIndexMap.put(getKey(data), rawList.size() - 1);
}
/**
* 将对象数组 转成 bitset
*
* @param dataList 对象集合
* @return bitset:对象在全部对象中的下标的压缩信息
*/
public BitSet buildBitSet(List<T> dataList) {
if (CollectionUtils.isEmpty(dataList)) {
BitSet bitSet = new BitSet();
return bitSet;
}
BitSet bitSet = new BitSet(rawList.size());
for (T data : dataList) {
Integer index = rawIndexMap.get(getKey(data));
if (Objects.nonNull(index)) {
bitSet.set(index);
} else {
log.warn("buildBitSet 异常, key={} 在全量原生数据map中没有找到", getKey(data));
}
}
return bitSet;
}
/**
* 将bitset 转成 对象集合
*
* @param bitSet bitset:对象在全部对象中的下标的压缩信息
* @return 对象集合
*/
public List<T> getDataList(BitSet bitSet) {
return bitSet.stream().mapToObj(e -> rawList.get(e)).collect(Collectors.toList());
}
private String getKey(T data) {
return getKeyFunction.apply(data);
}
}