前言
位图
bitmap在很多海量数据处理的情况下会用到,在数据过滤,数据位设置和统计等。海量用普通是数组超出数据保存范围。20亿颗恒星编号存储,我们只有2GB内存,如果用int数组,这2G全部占用完,int是4个字节,那么这20亿恒星编号占内存20亿 * 4byte /1024/1024/1024约7.5GB,如果改成位图存储20亿 /8/1024/1024/1024约0.233GB。充分利用数组索引来“存储”数据。java层面只能操作到byte,所以一次操作最少8bit。数组查找数据机制array[i]=base_address + i * data_size(数据类型大小)。如果实际内存存储的是0|1字节,而本身数据依靠映射的内存地址转换值,这样就大大减小了内存占用。
vector
平时我们用的1,2,3…数字只有大小,这些都是数量(标量),但在物理中最常见的速度,力都是既有大小有有方向,这些叫做向量(矢量)。 p ⃗ \vec p p= [ p 1 , p 2 , p 3 , . . . , p n ] T [p_1,p_2,p_3,...,p_n]^T [p1,p2,p3,...,pn]T。这是一个n维向量, p 1 , p 2 . . p_1,p_2.. p1,p2..也是向量,只是这些向量是从坐标原点出发的。每一个神经元都有一个方向的电流传导,不同神经元之间通过突出传递信息。n个神经元都在一个节点突出相连,那么它们最终的结果就是向量 p ⃗ \vec p p。
BitSet
在java中无法直接对bit(jvm层面的槽位)进行操作。底层都是0|1也可以把他们看作两个方向,如果把
p
⃗
\vec p
p中的都换成0|1
b
i
t
⃗
\vec {bit}
bit=
[
1
,
0
,
1
,
.
.
.
,
1
]
T
[1,0,1,...,1]^T
[1,0,1,...,1]T,那么这个位向量就出来了。所以可以用BitSet来代替。BitSet中的每一个元素其实就是二进制构成一个Vector,这些向量组成的集合就是BitSet。如果希望高效率地保存大量“开-关”信息,就应使用BitSet。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型的数组慢一些。
属性
/* BitSets are packed into arrays of "words." Currently a word is
* a long, which consists of 64 bits, requiring 6 address bits.
* The choice of word size is determined purely by performance
* concerns.
* 位向量被封装在`words`数组中,当前数组元素一个long类型,它有64位。需
* 要6个地址位(2^64).一个word大小选择完全取决于性能问题
*/
private static final int ADDRESS_BITS_PER_WORD = 6;
//内存结构64位,就不用考虑空白对齐问题,性能好
private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
//位索引掩码,0011 1111,确保索引最大不超过63
private static final int BIT_INDEX_MASK = BITS_PER_WORD - 1;
/* Used to shift left or right for a partial word mask
用来左移或右移部分word的掩码,保证每个word中数据不超出64位*/
private static final long WORD_MASK = 0xffffffffffffffffL;
//long类型数组存储二进制数据,每一个word占64个位,数组最大长度64
private long[] words;
/**
* The number of words in the logical size of this BitSet.
* 记录有效数组长度,有多少个word被使用
*/
private transient int wordsInUse = 0;
/**
* Whether the size of "words" is user-specified. If so, we assume
* the user knows what he's doing and try harder to preserve it.
* 保护初始化构造方法时自定义的数组长度
*/
private transient boolean sizeIsSticky = false;
构造器
一般构造器
把待处理数据中最大值拿来右移6位即除以64,这样就直到几个桶位才能存储下这些值了。
public BitSet() {
//默认初始化words数组,存储最大数字是64
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
/**
* Creates a bit set whose initial size is large enough to explicitly
* represent bits with indices in the range {@code 0} through
* {@code nbits-1}. All bits are initially {@code false}.
*
* @param nbits the initial size of the bit set
* @throws NegativeArraySizeException if the specified initial size
* is negative
* 一组数字中最大值
*/
public BitSet(int nbits) {
// nbits can't be negative; size 0 is OK
if (nbits < 0)
throw new NegativeArraySizeException("nbits < 0: " + nbits);
initWords(nbits);
sizeIsSticky = true;
}
/**一个word索引,这个索引其实就是需要存储数据值*/
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
/** 待存储数字中的最大值*/
private void initWords(int nbits) {
/**减一防止数字正好是2的幂次方,而多加一个word长度
* 加1避免最大数小于word长度64,而构建长度为0的数组
* 还有就是不是整数次幂,那么必须多一个word才能存下
*/
words = new long[wordIndex(nbits-1) + 1];
}
指定words构造器
用已经存在的数组,作为BitSet的words,最后几个连续的word必须非零。因为words按索引越大,代表的该值二进制位数(值=64*index+words[index])越高。最后一个元素就是所有数值的最高64位,为0浪费空间。
private BitSet(long[] words) {
//这里是BitSet内部数组,可不是真实想存储的值。
this.words = words;
this.wordsInUse = words.length;
checkInvariants();
}
//通过valueOf把一个long类型数组构造成BitSet
public static BitSet valueOf(long[] longs) {
int n;
/**long类型words数组,随着索引增大,他代表的二进制位数越高。
* 去掉高位的0节省空间。倒序遍历到第一个不为0的数,结束,获取
* 此时数组长度,作为BitSet的wordsInUse
*/
for (n = longs.length; n > 0 && longs[n - 1] == 0; n--)
;
//拷贝数组0~n(不包括数组从最后一位开始的连续0元素)。
return new BitSet(Arrays.copyOf(longs, n));
}
//例:此时会拷贝前四个元素构建BitSet。去除了末尾的0
long [] l = {1,4,0,10,0};
BitSet bs = BitSet.valueOf(l);
BitSet中words模型

Long.numberOfLeadingZeros
long类型最从高位开始第一个不为0的位之前连续0的个数。这是十进制的10的二进制表示。Long.numberOfLeadingZeros(10)=60最高位连续60个零。这也可以用来求该数最高位第一个1的位置:64-Long.numberOfLeadingZeros(10)第四位。

public static int numberOfLeadingZeros(long i) {
//无符号右移32,取出高32位
int x = (int)(i >>> 32);
/**如果高32位是0,那么i就小于2147483647,已有32位0,在加上低32位的
* 最高位连续0。如果高32不为零直接计算高32的numberOfLeadingZeros。
*/
return x == 0 ? 32 + Integer.numberOfLeadingZeros((int)i)
: Integer.numberOfLeadingZeros(x);
}
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
//i等于0,就是32个0返回32,负数,最高位一定是1,所以没有连续0永远返回0
if (i <= 0)
return i == 0 ? 32 : 0;
//已经排除了i等于0,即32个0的情况,所以最多31个0,即i=1。
int n = 31;
//二分法
/**i大于等于32768,低16位就装不下了,所以高16位至少有一位被占,最多
* 15个0。15=(n-=16),无符号右移16位取出高16位。交给下一步处理高16
* 位Hight_16
*/
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
/**
* 对Hight_16,如果大于128,则Hight_16的低8位装不下,最高位开始连续0
* 又减少8位。无符号右移取出Hight_16的高8位交由下一步处理
* Hight_16_Hight_8,n=7
*/
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
/**
* 对于Hight_16_Hight_8,如果大于等于64,则Hight_16_Hight_8的低4位
* 存储不下,所以最高位开始连续0个数又减少4位。无符号右移4位交由下一
* 步处理Hight_16_Hight_8_Hight_4,n=3
*/
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
/**
* Hight_16_Hight_8_Hight_4,如果大于等于4,
* Hight_16_Hight_8_Hight_4的低两位存不下,所以最高位开始连续0个数
* 又减少2位n-=2,无符号右移两位交由下一步处理
* Hight_16_Hight_8_Hight_4_Hight_2,n=1
*/
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
/**
* 拿出Hight_16_Hight_8_Hight_4_Hight_2的高位(无符号右移1位),要么
* 0要么1,如果是1则n-1,是0,n值不变。例如如果是10.二进制1010,经过
* 上面的if就剩下10,此时n=31-2=29,又要减去一个。其实这里就是二分法
* 处理到第一个不为零的位和它下一位,最后一次分割判断高位是否为1还是0
*
*/
return n - (i >>> 1);
}
新增
在这里插入代码片

本文介绍了BitSet在海量数据处理中的优势,如节省内存和高效操作。讲解了BitSet的属性,如word大小、位索引掩码,以及构造器如根据初始大小和指定words的用法。重点剖析了Long.numberOfLeadingZeros方法在BitSet中的作用。
915

被折叠的 条评论
为什么被折叠?



