jason's blog


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

BitMap、BitSet与Bloom Filter

发表于 2018-10-08 | 分类于 Java

推荐阅读时间:5分钟

引言

先来看一个问题,假设现在有范围为 1-10 亿的 11 亿个 int 型整数,如何排除掉其中的重复数字?int 占 4 个字节,可以表示 -2,147,483,648 ~ +2,147,483,647 的数据。
所以一般的思路是会将这 11 亿个数作为 int 型数据存到一个 HashSet 集合中进行去重。但是,这样会存在一个问题。我们知道 1GB=1024KB=1024 1024Byte,那么 10亿 4Byte 将占用接近 4GB 的内存,这将是极大的性能浪费,很可能会影响程序的健康运行。

思路

我们可以考虑用”位映射(BitMap)”来解决这个问题。试想一下,如果我们有一个位数组(bit[n]),那么我们可以用 bit[i] 来表示 0-n 中对应的数字,每个元素有 1 和 0 两个取值,分别代表该数字存在与否。这样一来,我们记录一个数字只需要一个 bit,相对于之前的 int 类型(32 bit),整整缩小了 32 倍的存储大小(4GB/32=125MB)!

扩展一下,当我们需要记录每个数字出现次数是否超过 2 次时,可以使用连续的两位来记录一个数字:一位用来记录是否出现,另一位用来记录是否超过 2 次。

不过,Java 中无法创建 bit 数组,我们可以使用 int 或 long 数组来实现这个目的。建立一个 int 数组 int[n],int[0] 记录了 0-31,int[1] 记录了 32-63 …… 依此类推。

Java BitSet

Java 中有一个 BitSet 类,从命名就可以看出它是用来存储去重的位元素集合,它还支持 and、or 等位运算。用来解决文章开头的问题非常合适,方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BitSetStudy {
public static void main(String[] args) {

BitSet bitSet = new BitSet(1000000000);
//随机生成 11 亿个数字
for (int i = 0; i < 1100000000; i++) {
bitSet.set((int) (Math.random() * 1000000000));
}

System.out.println(bitSet.size());
System.out.println("end");

}
}

打开 jconsole,可以看到存储了 bitset 对象的老年代所占用的内存稳定在 125MB 左右。

关于 BitSet 的 and、andNot、or、xor 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
BitSet bitSet = new BitSet();
bitSet.set(2, 6);
BitSet bitSet1 = new BitSet();
bitSet1.set(4, 8);

BitSet bitSetAnd = (BitSet) bitSet.clone();
BitSet bitSetAndNot = (BitSet) bitSet.clone();
BitSet bitSetOr = (BitSet) bitSet.clone();
BitSet bitSetXor = (BitSet) bitSet.clone();

bitSetAnd.and(bitSet1);
bitSetAndNot.andNot(bitSet1);
bitSetOr.or(bitSet1);
bitSetXor.xor(bitSet1);

System.out.println("bitSet is : " + bitSet + " , and bitSet1 is: " + bitSet1);
System.out.println("run bitSet.XMethod(bitSet1) , result is : ");
System.out.println("and:" + bitSetAnd);
System.out.println("andNot:" + bitSetAndNot);
System.out.println("or:" + bitSetOr);
System.out.println("xor:" + bitSetXor);

}

结果如下:

1
2
3
4
5
6
bitSet is : {2, 3, 4, 5} , and bitSet1 is: {4, 5, 6, 7}
run bitSet.XMethod(bitSet1) , result is :
and:{4, 5}
andNot:{2, 3}
or:{2, 3, 4, 5, 6, 7}
xor:{2, 3, 6, 7}

自己实现 BitMap

Java 的 BitSet 使用起来存在局限性,我们可以参考 BitSet 实现自己的 BitMap 来扩展应用场景。核心还是通过 int/long 数组元素的位来记录有序数据,一个实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BitMap {
private int[] words;

private int size;


public BitMap(int n) {
size = n;
//每个int占32位,数组大小为 n/32+1
words = new int[(size >> 5) + 1];
}

public final void set(int bit) {
//获得数据所在的数组序号(int),相当于 bit/32
int i = bit >> 5;
//获得该int元素中要修改为1的数字,相当于 bit%32
int j = bit & 31;
//获得要修改的位
int k = 1 << j;
//将该元素相应的二进制位设为1
words[i] |= k ;
}

public final boolean get(int bit) {
//参考set方法理解
return (words[bit >> 5] & (1 << (bit & 31))) != 0;
}
}

实现了核心的存储结构、set 和 get 方法。

布隆过滤器(Bloom Filter)

了解完 BitMap,我们掌握了一种处理大量连续数据的好方法。现在继续扩展一下,现在如果我们要记录的是海量的分布很不均匀的数据,如果继续用 BitMap 的方式,将会浪费大量的存储空间,或者数据量已经大到使用 BitMap 的方式,仍然会浪费大量的内存空间。面对这两种情情况时,我们可以考虑使用布隆过滤器。

布隆过滤器核心是对一个 key 使用多个 hash 函数求出多个值,将这些值散列在一个有限的数组中,这样,当这些 hash 函数求出的值都符合预期就认为该 key 存在;若只要存在 hash 函数的值不符合,就可以确定它不存在。在某些场景下,这种方法效果非常好。图示如下:

可以看出来,这种方法存在一定误差,不过误差几率可以通过增加 hash 函数和散列数组大小来减小。还有一个问题就是,当某个 key 被删除后,不能直接在散列表中将对应的 value 去掉,因为可能会影响其他 key。针对这个问题,我们可以维护一个黑名单,每次计算 hash 前,先判断 key 是否在黑名单中,有则表示该 key 已删掉。

Java 8 HashMap(上)—— 红黑树

发表于 2018-09-24 | 分类于 Java

推荐阅读时间:10分钟

简介

Java8的最大特性是使用红黑树结构来存储每个(桶)bucket中的数据(当链表长度超过8时)。
为什么引入红黑树呢?其实正常情况下,平均每个桶中应该只会有不到1个数据,但当发生大量Hash碰撞时,每个桶中的数据也将会大量增加,这将会影响到数据的查询速度。在Java7中,每个桶使用链表存储数据,查找数据采用遍历的方式,查询时间复杂度为O(n)。Java7中为了避免大量Hash碰撞的问题,引入了hashseed方式,增强了Hash函数的散列性。但是randomHashSeed方法调用的next方法在多线程运算时存在性能问题(待考证),故在Java8中被弃用。Java8中换了一个思路:使用红黑树来提高查找速度(O(logN)),即使发生大量hash碰撞也不会造成性能影响,这便是红黑树由来的原因。
Java7的HashMap一共有接近1200行代码,而到了Java8直接增加到了2400行,减去全局变量前新加的100行注释,相差1100行。其中TreeNode(红黑树)实现部分有600行代码,再加上其他方法对红黑树的适应性改动,可见红黑树部分是Java8 HashMap的主要改动。

详情

继承关系如下:

TreeNode继承了LinkedHashMap.Entry,LinkedHashMap.Entry继承了HashMap.Node,而Node其实就是上个版本的Entry(链表)结构。此处有个疑惑:TreeNode并没有使用LinkedHashMap.Entry的before和after字段,不知道为啥不直接继承Node类。

字段和构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//常见的树结构
HashMap.TreeNode<K, V> parent;
HashMap.TreeNode<K, V> left;
HashMap.TreeNode<K, V> right;
HashMap.TreeNode<K, V> prev;
boolean red;

TreeNode(int hash, K key, V val, HashMap.Node<K, V> next) {
super(hash, key, val, next);
}
}

红黑树全部方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
/**
* 获取根结点
*/
final HashMap.TreeNode<K,V> root() {
for (HashMap.TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}

/**
* 选取根结点
* 红黑树进行左旋、右旋后,根结点可能移动,头结点需要重新指向新的根结点
*/
static <K,V> void moveRootToFront(HashMap.Node<K,V>[] tab, HashMap.TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
HashMap.TreeNode<K,V> first = (HashMap.TreeNode<K,V>)tab[index];
//如果根结点不是桶的第一个节点,则将根结点移动到头结点位置
if (root != first) {
HashMap.Node<K,V> rn;
tab[index] = root;
HashMap.TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((HashMap.TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
//断言红黑树结构是否正常,不正常则报异常停止
assert checkInvariants(root);
}
}

/**
* 根据结点hash值、key和key的类型 进行红黑树查找
*
*/
final HashMap.TreeNode<K,V> find(int h, Object k, Class<?> kc) {
HashMap.TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
HashMap.TreeNode<K,V> pl = p.left, pr = p.right, q;
//待查询结点的key的hash值若小于当前结点则进入左子树,否则进入右子树
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
//如果hash值相等再比较key,一致则表示找到,返回结果
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
//如果key不同,则看key的类型是否可比较,
//如果可以比较则根据比较结果选择是否返回查询结果,还是继续查询左、右子树
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}

/**
* 根据结点hash值、key 进行红黑树查找
*/
final HashMap.TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}

/**
* 强制比较两个结点的key
*
* 当两个类型不能直接比较时,通过对类名进行hash进行比较;若hash值也相等,判定b大。
* 故一定会排出先后顺序。
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}

/**
* 将链表转化为红黑树
* 重要方法!当链表长度增加到红黑树转换阈值(默认8),且桶的数量不小于64 时触发
*/
final void treeify(HashMap.Node<K,V>[] tab) {
HashMap.TreeNode<K,V> root = null;
//遍历链表
for (HashMap.TreeNode<K,V> x = this, next; x != null; x = next) {
next = (HashMap.TreeNode<K,V>)x.next;
x.left = x.right = null;
//首结点作为根结点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (HashMap.TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//如果未能比较,调用强制比较方法,确保有序
dir = tieBreakOrder(k, pk);

HashMap.TreeNode<K,V> xp = p;
//当左或右子树为空时,插入链表上的一个结点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//插入结点后,进行左旋、右旋重平衡,转换为红黑树
root = balanceInsertion(root, x);
break;
}
}
}
}
//确保根结点是首结点
moveRootToFront(tab, root);
}

/**
* 红黑树转换为链表
* 当红黑树结点减少到链表还原阈值(默认6)时触发
*/
final HashMap.Node<K,V> untreeify(HashMap<K,V> map) {
HashMap.Node<K,V> hd = null, tl = null;
for (HashMap.Node<K,V> q = this; q != null; q = q.next) {
HashMap.Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}

/**
* 红黑树插入新结点
*/
final HashMap.TreeNode<K,V> putTreeVal(HashMap<K,V> map, HashMap.Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
HashMap.TreeNode<K,V> root = (parent != null) ? root() : this;
for (HashMap.TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
HashMap.TreeNode<K,V> q, ch;
searched = true;
//插入结点与当前结点hash值相等时,查找左子树或右子树,若已包含待插入结点则直接返回结果
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}

HashMap.TreeNode<K,V> xp = p;
//若已比较到左子树或右子树为空时还没有找到,则插入该结点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
HashMap.Node<K,V> xpn = xp.next;
HashMap.TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((HashMap.TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}

/**
* 删除红黑树结点
* 呃,这个方法和前面的比起来大同小异,不细看了
*/
final void removeTreeNode(HashMap<K,V> map, HashMap.Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
HashMap.TreeNode<K,V> first = (HashMap.TreeNode<K,V>)tab[index], root = first, rl;
HashMap.TreeNode<K,V> succ = (HashMap.TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
//若删除结点后,达到链表还原阈值,则还原为链表
tab[index] = first.untreeify(map);
return;
}
HashMap.TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
HashMap.TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
HashMap.TreeNode<K,V> sr = s.right;
HashMap.TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
HashMap.TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
HashMap.TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}

HashMap.TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

if (replacement == p) { // detach
HashMap.TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}

/**
* 将红黑树分为两半
* 扩容时,会将一个桶中的红黑树拆分为两个;若拆分后红黑树不够大,会被还原为链表。
*/
final void split(HashMap<K,V> map, HashMap.Node<K,V>[] tab, int index, int bit) {
HashMap.TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
HashMap.TreeNode<K,V> loHead = null, loTail = null;
HashMap.TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (HashMap.TreeNode<K,V> e = b, next; e != null; e = next) {
next = (HashMap.TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}

if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}

/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR

/**
* 红黑树的左旋
* 红黑树相关知识待深入学习后进一步探讨
*/
static <K,V> HashMap.TreeNode<K,V> rotateLeft(HashMap.TreeNode<K,V> root,
HashMap.TreeNode<K,V> p) {
HashMap.TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}

/**
* 红黑树的右旋
*/
static <K,V> HashMap.TreeNode<K,V> rotateRight(HashMap.TreeNode<K,V> root,
HashMap.TreeNode<K,V> p) {
HashMap.TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}

/**
* 插入结点后进行重平衡
* 这部分只看懂代码了,算法比较懵,有机会再探讨红黑树算法
*/
static <K,V> HashMap.TreeNode<K,V> balanceInsertion(HashMap.TreeNode<K,V> root, HashMap.TreeNode<K,V> x) {
//将插入的结点设为红色
x.red = true;
for (HashMap.TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//x为根结点时,设为黑色,直接返回
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//父结点为黑且为根结点时,直接返回。
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//父结点为祖父结点的左孩子结点时:
if (xp == (xppl = xpp.left)) {
//祖父结点的右孩子结点为红色时,叔叔结点和父结点置黑,祖父结点置红,设置当前结点为祖父结点
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
//否则,如果当前结点为父结点的右孩子结点,进行左旋
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父结点不为空设为黑色,且如果祖父结点不为空则设为红色并进行右旋
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 删除结点后进行重平衡
*/
static <K,V> HashMap.TreeNode<K,V> balanceDeletion(HashMap.TreeNode<K,V> root,
HashMap.TreeNode<K,V> x) {
for (HashMap.TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (x.red) {
x.red = false;
return root;
}
else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
if (xpr == null)
x = xp;
else {
HashMap.TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
HashMap.TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}

/**
* 检查红黑树结构是否正常
*/
static <K,V> boolean checkInvariants(HashMap.TreeNode<K,V> t) {
HashMap.TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (HashMap.TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}

Java8的HashMap解读暂告一段落,下期继续探讨其他特性,希望不要太久😂

https://y.qq.com/n/yqq/song/004dbfuf1jEjpI.html#stat=y_new.index.new_song.songname

Java 8 HashMap(下)—— compute

发表于 2018-09-24 | 分类于 Java

推荐阅读时间:5分钟

简介

本篇接上篇 Java 8 HashMap (上)—— 红黑树,
继续探讨 Java8 的HashMap 的新特性。内容不多,重点介绍 compute 方法。

compute

compute 方法主要用来将一个复杂计算的结果作为值赋给指定的键。key 指待修改或插入的键,remappingFunction 是一个 BiFunction 对象。
BiFunction 是指一个 二元(binary)函数;定义时,指定两个入参字段类型和一个结果字段类型。通过实现 apply 方法来自定义计算逻辑。

BiFunction 部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
public interface BiFunction<T, U, R> {

/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
}

compute 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
//根据 key 值,找到对应的 旧值
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
//计算出新值
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
//如果新值为 null,删掉这个 Node
removeNode(hash, key, null, false, true);
}
//如果新值不为 null,替换这个 Node
else if (v != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}

computeIfAbsent、computeIfPresent 方法与 compute 类似,分别表示当相应键的值缺席、存在时,才进行 compute 方法。
源码与 compute 基本一致,不赘述。

demo 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
Map<String, String> hashMap = new LinkedHashMap<>();
hashMap.put("k1", "v1");
hashMap.put("k2", "v2");
hashMap.put("k3", "v3");
Function<String, String> function = new Function<String, String>() {
@Override
public String apply(String s) {
return "test_" + s;
}
};
BiFunction<String, String, String> biFunction = new BiFunction<String, String, String>() {
@Override
public String apply(String s, String s2) {
return "test_" + s + "_" + s2;
}
};
hashMap.compute("k1", biFunction);//生效
hashMap.compute("k11", biFunction);//生效
hashMap.computeIfAbsent("k2", function);//不生效
hashMap.computeIfAbsent("k21", function);//生效
hashMap.computeIfPresent("k3", biFunction);//生效
hashMap.computeIfPresent("k31", biFunction);//不生效
System.out.println(hashMap);
}

结果:

1
{k1=test_k1_v1, k2=v2, k3=test_k3_v3, k11=test_k11_null, k21=test_k21}

merge

merge 方法是根据旧值进行计算,修改相应 Node。与 compute 结构相似,不过 BiFunction 入参为 oldValue,newValue。
源码与 compute 基本一致,不赘述。

其他

其他新增的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}

@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}

@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
  1. getOrDefault 方法根据 key 获取相应的值,如果为 null,则返回设置的默认值。
  2. putIfAbsent 方法会在 指定的 key 不存在时,插入相应的值。
  3. remove(Object key, Object value) 方法会根据 key 和 value 进行删除,如果 相应 key 的值不为该 value,则不执行删除并返回 false。
  4. replace(K key, V oldValue, V newValue) 与 replace(K key, V value) 都是替换值的操作,具体区别与上述方法类似。

最后

HashMap历时一个半月,终于告一段落了😂。

由于在源码分析系列含有大量代码,放在公众号上不方便阅读,后续写的话会发在博客上。有兴趣的欢迎访问 http://jasonsonghoho.github.io 。


“你不知
这风雪一程
有太多不易”

Java 7 HashMap 源码解读

发表于 2018-08-18 | 分类于 Java

见链接:https://mp.weixin.qq.com/s/nHk9jKapNVVkBDnKChgK0Q

React快速入门

发表于 2018-06-24 | 分类于 前端

见链接:https://mp.weixin.qq.com/s/fDXQe_QCBN4q6zfzKQr1jQ

动物园管理员——ZooKeeper

发表于 2018-06-05 | 分类于 中间件

见链接:https://mp.weixin.qq.com/s/hnfWq1sHD8qxKoXHKwJhiw

MySQL究竟如何解决“不可重复读”和“幻读”的

发表于 2018-05-27 | 分类于 数据库

见链接:https://mp.weixin.qq.com/s/Ej3coEuouPqbzkL0-J8tIQ

我们为提升 Cassandra 读性能做了哪些努力?

发表于 2018-05-21 | 分类于 数据库

目录 (Table of Contents)

  • 关于Cassandra
  • 提升读性能

关于Cassandra

Apache Cassandra是一个高度可扩展的高性能分布式数据库,
用于处理大量常规服务器上的大量数据,提供高可用性,无单点故障。它是一种NoSQL类型的数据库。

我们看一下国外权威机构DB-Engines最近的数据库全球流行程度排名:

可以看出,Cassandra 是排名前十中四个仅有的NoSQL数据库之一。Cassandra在国外这样受欢迎,其性能可想而知不会差,
但是在国内貌似还没有多少公司使用,且国内关于 Cassandra方面的资料较少。

提升读性能

Cassandra 在我们的项目中用来存储时序数据,经过测试,在三台4核16G的虚拟机上,
指标数据的写入TPS可以达到6.5W/s,基本可以满足我们的业务需求。
但是读取性能可能就会差很多,因为数据查询速度跟每次查询的数据量关系比较大,此处也不好定义TPS。
产品查询一周以上的指标数据时,经常会出现加载缓慢,甚至查询超时。为了改善查询状况,我们进行了不少努力。

此处不讨论纵向扩展和横向扩展带来的性能提升。

1. 加快墓碑回收甚至去除墓碑

在Cassandra中,当你删除一条数据时,其实是给这条数据进行update,给它update上一个标识,就是一个墓碑标识。
当Cassandra集群在不同节点之间同步删除信息的时候,也会用到Tombstones(墓碑),可以说墓碑是一种允许Cassandra快速写入的机制。

关于墓碑的更多消息,可参考 Cassandra 数据维护官方文档

可以这样理解,大量的墓碑数据会使查询时搜索的数据量变大,直接影响查询时的效率。
所以,为了消除这种影响,我们可以加快墓碑数据的回收,避免产生大量的墓碑数据。
甚至,当我们在写入时,若写入一致性的值与副本因子数量相等时,可以不产生墓碑数据,直接删掉该无效数据。
具体可通过 调整 table 中 gc_grace_seconds 参数来实现,默认为 864000(10天),我们可以设为 86400(1天)或者0(直接删除)。

2. 降低read repair 的几率

每一次读操作,Cassandra都会在后台进行read repair操作。
如果只要求读一个节点数据,Cassandra在读到一个节点后,就将结果返回客户端,
然后用read repair对其他的replicas进行同步(根据timestamp)。
如果要求读多个节点,那么Cassandra就读多个节点,然后根据timestamp进行比较,返回客户端最新的数据,
然后再调用read repair对其他节点进行同步。
Read repair在后台的操作,会占用一定的CPU和I/O,所以影响读性能。
我们可以降低read repair 的几率,以提高读取性能。

通过修改 table 中 read_repair_chance(取值范围 0-1)参数来设置read repair 的几率,建议设为 0.1。

3. 指标数据预聚合

思路:我们存在数据库中的指标数据,读取时会将指定时间范围内的数据进行聚合。
如果提前将数据按基本时间段提前聚合为一个值,读取时,只读取时间范围内的时间段的汇聚结果,将大大减少查询耗时。

4. 合理部署产品

公司的其他产品使用了mongoDB,线上环境中发现,这两个NoSQL数据库部署在一起时会相互争夺内存资源,
十分影响性能,因此最好将这两个数据库分开部署。

5. 设置合理的堆内存大小和GC策略

堆内存设置的太小,将导致频繁GC甚至OOM,设置的太大同样也不好。可参考官方的公式:
MAX_HEAP_SIZE=max(min(1/2 ram, 1024MB), min(1/4 ram, 8GB)

关于GC策略,官方的建议是:小于16G,用CMS收集器;16-64G,用G1收集器。

6. 设置合理的压式策略

Cassandra 将落到磁盘的数据存放在SStable中,压实是将多个SSTable 文件合并为一个的过程。合并后将减少重复的数据,使数据更紧凑。
Cassandra 有多种触发压实的策略,选一个适合的压实策略,可以更好地压实数据。
比如,我们使用的Kairosdb建议采用 DateTieredCompactionStrategy (DTCS))压实策略。

7. 启用 row cache

row cache 把整个row 的内容都放在内存中。
适合的情况是,有一小部分hot data是经常反问的,或者要返回整个columns.在使用row cache时,用注意它对内存的影响。
可参考 Cassandra 的官方文档设置row cache。

其他

安利一个入门Cassandra 的好博客:learn Cassandra

Linux中的top命令详解

发表于 2018-05-20 | 分类于 Linux

见链接:https://mp.weixin.qq.com/s/xyYn6xaahQ4hv1y9MM-7JA

Linux 的 Cache、Buffer、MemAvailable、Swap简介

发表于 2018-05-20 | 分类于 Linux

见链接:https://mp.weixin.qq.com/s/59oH1hMMXy6YC618gZkm1g

1234
jason song

jason song

创造美好!

32 日志
10 分类
45 标签
RSS
Links
  • 阿里中间件BLOG
  • Flink China
© 2020 jason song
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4