hashMap 在 1.7 和 1.8 中的区别
1、 引入了红黑树
2、解决了并发环境下,死链的情况(在1.7的rehash中,会进行链表的反转插入,会引发死链问题,java8 中不会,8中是采用修改头部节点的位置来实现)
3、1.7 扩容发送在,插入数据之前,会先检查,扩容后插入。 1.8是在插入数据之后,在判断是否需要扩容。
4、为什么1.8解决了死链问题还是不安全,在多线程的情况下,可能会put遗漏数据。
ConcurrentHashMap
hashmap固然非常优秀,但是在高并发的情况下,他存在着一些不足的情况。所以这里来分析一下ConcurrentHashMap的原理
sizeCtl
英语好,胜过一切
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
含义有如下3中情况:
- -1 表示正在有线程进行初始化
- '>0 的情况,标识map的负载容量
- 负数(非 -1) 代表有几个线程正在扩容,例如 -2 有一个线程在扩容
ConcurrentHashMap
cas +sync 的方式来代替segment
1、并发扩容
2、通过数组的方式来统计并发情况下的元素增加
addCount();
分片方式
base + 采用了数组累加的方式,
用ThreadLocalRandom 来获取,随机数,来确定数组的位置,
解决并发问题引起的数值大小问题。例如 LongAddr 采用的是同一个思想
putvalue
增加了sync ,锁住了头结点,相比于1.7的分段锁,锁的粒度更细了。
扩容
chm的扩容是可以多个线程并行来扩容的。
transfer
1、扩大数据
2、转移数据链
分片思维,设置分工区间。
把链表进行分类 n为当前链表长度 :
a hash & n = 0
b hash & n != 0
ConcurrentHashMap
阶段一
HashMap 是开发中高频集合。可看另一篇文章####
怎么去解决它的并发问题:
1、HashTable
2、Collections.synchronizedMap(hashMap)
3、ConcurrentHashMap
1、2 方法类似,都是对对象增加一个锁,由于锁粒度粗,所以效率并不高。
阶段二
1.7的 ConcurrentHashMap
在1.7中,ConcurrentHashMap 采用分段式锁来控制并发。
Segment 继承ReentrantLock充当锁的角色,每个Segment,负责一部分的桶。比如一个Segment 负责8个桶,那么当有值落在这几个桶内时,需要先获取Segment对象,否则需要先等待。
相比于HashTable的全局锁,Segment的粒度更细,所以在并发环境下,性能更好。
1.8的 ConcurrentHashMap
在1.7把锁细化后,可能是觉得效果不错,于是在1.8中,通过cas和Synchronized来更加细化锁的粒度,每个桶使用一个锁。同时引入了红黑树的结构。
至于为什么使用Synchronized而不是lock可能是sync又进行了一波升级。
Synchronized 的重要改变是在1.6后区分了偏向锁、轻量级锁、重量级锁。提升了整个java内置锁的效率。
阶段三 源码分析(1.8)
重要参数
- Node<K,V>[] table : 当前的node数组,也就是桶数组。默认为null
- Node<K,V>[] nextTable:默认为null,扩容时新生成的数组
- sizeCtl:是这个ConcurrentHashMap中一个很关键的属性,先看下源码中的注释:
Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
中文可以翻译如下几个点:
- 当为-1时,容器正在初始化
-
0 时,代表容器的容量
- 当为 非-1 的负数时,代表有几个线程正在扩容。如 -2 时,代表一个线程在扩容
4.当table未初始化的时候,表示初始化后的大小。
- Node 单位数据结构如下
class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}
- ForwardingNode 一个特殊的Node节点,hash值为-1,只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动
map 长度
由于 这个类的get和put很复杂,但是所使用的思想是一致的,所以这里先分析一下size的维护。
总体思想:size的维护采用LongAddr的方法,使用一个base+数组的方式来统计最后的长度。当并发情况下,采用数组中不同的桶,来记录数量的变化,最后再累加起来。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
// 当数组不为null时,采用累加的方式统计
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
同时,在添加个数时,如下:
// x 为添加的个数,通常为1
// check 分2种情况,
//当 check< 0 ,不检查是否在扩容情况下
// 当 check <= 1 ,只检查是否存在竞争,多个线程在插入数据
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 分界线 以下情况,说明在添加个数时,整个map正在扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
上面代码比较多,可以总结如下2个功能:
1、增加元素个数
2、判断元素是否需要扩容
第一个if里面的。主要做了如下的事,在数组中选择一个下标用来存储,然后判断改下标是否存在竞争,然后具体操作交给fullAddCount
方法来。分析如下:
1、counterCells != null 初始化完成
2、通过cas对basecount增加
3、获得随机下标
4、判断这个下标的竞争状态
5、调用fullAddCount 方法来完成计数
fullAddCount 方法如下:
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// --- 判断是否是随机,不是随机的话,随机取一个值
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// 自旋操作
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 1 进入时counterCells已经被初始化后,走下面的逻辑
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
// 2 当counterCells 还没有被初始化时,走入下链路
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 3 通过cas basecount就添加成功
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
为了清晰一点,单独先拿2出来代码比较:
//
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
代码的功能流程如下
1、检查是否是第一次进入 cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)
2、初始化CounterCell 数组 CounterCell[] rs = new CounterCell[2];
3、在随机的下标中,添加rs[h & 1] = new CounterCell(x);
构造函数
//使用了sun.misc.Contended 来防止伪共享的存在
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
此时,再回过头来看1中的代码,就是在已经有数组的情况下增加计数,不做累述。
阶段四、个人愚见
到目前为止,虽然只分析了map中,size的维护方式,采用了数组分片的方式来处理并发。但是可以想见,map在后续的方法中也会采用这种思想来完成高并发下的安全。也许可以在日后的日常开发中运用起来这种方式。