Java集合——Map篇
- HashMap
- ConcurrentHashMap
- jdk1.7
- jdk1.8
HashMap
- jdk1.7 数组+链表
- jdk1.8 数组+链表/红黑树
- 默认数组大小:16,扩容后或者指定初始大小都要是成2的N次幂,原因是数组扩容后,要对原来的数据重新进行定位,定位方式是 hash值 & (length-1),length是2的N次幂的话,就等于 hash值 % length
- 指定数组大小,实际大小为
- 默认阈值:0.75
- 默认情况下至少要有12个对象进入集合才可能扩容,说是可能,是因为可能会形成链表。(这12指的是数组被占用了12个,而不是集合中有12个对象。)
- jdk1.7 头插法,并发扩容时可能会形成链环。
- 假设数组中某一节点存在 3->7->null 这样的链表,这时需要扩容;
- a线程此时进入下面代码,在注释1处卡住了;此时 e = 3, next = 7
- b线程此时也并发进行扩容,且完成扩容,假设扩容后3,7还在同一数组的节点链表上,采用头插法后此时该节点存在7->3->null 的链表并已经进入堆内存线程共享。
- a线程被唤醒,此时扩容的新数组newTable还在栈内存私有,但是原数组table因为b线程发生了改变,此时b线程扩容后的节点链表为 7->3->null,a线程的e指向的是这里的3,next指向的是这里的7,
- a线程接着运行,第一遍循环后,新数组newTable的链表为3->null,e 为b线程扩容后的节点链表的7,next也为7
- 第二遍循环开始,next赋值为e.next,即为7的next 即为 3这个节点,第二遍循环结束,此时新数组newTable的链表为 7->3>null,e 为b线程扩容后的节点链表的3,next也为3
- 第三遍循环开始,next赋值为e.next,即为3的next 即为null,注意,注释2处 e.next = newTable[i]; 意味着 3节点的next为新数组newTable的7这个节点,而第一二遍循环后新数组newTable的链表为 7->3>null,此时有3->7,从而造成了链环。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next; // 1 a线程卡住时 此时e = 3, next = 7
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 2
newTable[i] = e;
e = next;
}
}
}
jdk1.8 尾插法,扩容采用高低位移动,避免并发扩容时可能会形成链环。
ConcurrentHashMap
jdk1.7
数据结构:Segement数组 + HashEntry数组 + 链表
- 特点:每次查找key都需要hash两次,一次找到Segement数组对应位置,一个找到HashEntry数组对应位置。
锁机制:Segement 继承 ReentrantLock,put、size、remove等操作使用分段锁,get不上锁。
- 分段锁:对Segement数组中的一个Segement对象上锁,不影响在其他Segement对象上的HashEntry。
- size:该方法是将Segement数组遍历一遍,将每个Segement中的count相加,count在每次put、remove时都会有相应修改。
插入方式:头插法
初始化和扩容:
- 初始化每个Segement的HashEntry数组大小为2。
- 只针对单个Segement的HashEntry数组成倍扩容。
- 对Segement数组的对象个数固定,默认为16。
// initialCapacity默认16,loadFactor默认0.75,concurrencyLevel默认16
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
// ssize值是最近大于等于concurrencyLevel的2的幂次方,默认16
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// MIN_SEGMENT_TABLE_CAPACITY 默认为2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
// ssize值决定Segment数组的大小,且以后不会再改变
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
jdk1.8
数据结构: Node数组 + 链表/红黑树
- 特点:当链表长度为8且数组容量>=64时,转换成红黑树结构。
锁机制:synchronized + CAS,synchronized 只对单个Node上锁
初始化和扩容:
- Node数组默认数组大小为16,容量为12,无参构造方法是空实现,是在第一次put方法里,初始化Node数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY默认16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sc是12
sc = n - (n >>> 2);
}
} finally {
// sizeCtl 默认可用大小(容量)为12
sizeCtl = sc;
}
break;
}
}
return tab;
}
- 并发扩容
//新增元素时,也就是在调用 putVal 方法后,为了通用,增加了个 check 入参,用于指定是否可能会出现扩容的情况
//check >= 0 即为可能出现扩容的情况,例如 putVal方法中的调用
private final void addCount(long x, int check){
... ...
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//检查当前集合元素个数 s 是否达到扩容阈值 sizeCtl ,扩容时 sizeCtl 为负数,依旧成立,同时还得满足数组非空且数组长度不能大于允许的数组最大长度这两个条件才能继续
//这个 while 循环除了判断是否达到阈值从而进行扩容操作之外还有一个作用就是当一条线程完成自己的迁移任务后,如果集合还在扩容,则会继续循环,继续加入扩容大军,申请后面的迁移任务
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
// sc < 0 说明集合正在扩容当中
if (sc < 0) {
//判断扩容是否结束或者并发扩容线程数是否已达最大值,如果是的话直接结束while循环
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);
}
//如果集合还未处于扩容状态中,则进入扩容方法,并首先初始化 nextTab 数组,也就是新数组
//(rs << RESIZE_STAMP_SHIFT) + 2 为首个扩容线程所设置的特定值,后面扩容时会根据线程是否为这个值来确定是否为最后一个线程
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}