concurrentHashMap hashTable源码分析与比较
线程安全的Map共经历了三个过程,直接在方法上增加synchronized方法,segment段实现减少锁的粒度,cas(当前内存中的值V和旧的预期值A是否相等,如果相等则将新的值B赋值给V)锁实现。
https://blog.csdn.net/dianzijinglin/article/details/80997935 hashtable解析
https://www.cnblogs.com/dolphin0520/p/3932905.html 1.6版本concurrentHashMap实现
https://blog.csdn.net/jianghuxiaojin/article/details/52006118
https://www.jianshu.com/p/d10256f0ebea 1.8版本concurrentHashMap实现
Hashtable源码解析:
直接在方法上加synchronized方法,锁的粒度特别大
public synchronized V put(K key, V value) {
//确保value不为null
if (value == null) {
throw new NullPointerException();
}
//确保key不在hashtable中
//首先,通过hash方法计算key的哈希值,并计算得出index值,确定其在table[]中的位置
//其次,迭代index索引位置的链表,如果该位置处的链表存在相同的key,则替换value,返回旧的value
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
// 已有的key不在map中,校验是否需要rehash
modCount++;
if (count >= threshold) {
//如果超过阀值,就进行rehash操作
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
//将值插入,返回的为null
Entry<K,V> e = tab[index];
// 创建新的Entry节点,并将新的Entry插入Hashtable的index位置,并设置e为新的Entry的下一个元素
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;
}
map中冲突的新节点放到首部还是尾部?
1.7以前是放到头部(原因:热数据放前面)
问题:rehash失效,且头部可能导致rehash死循环,且判断是否存在需要遍历到尾部 所以1.8中已经修改为尾部
hashMap为什么进行树化?是什么树?为什么不是别的树?
避免大量hash碰撞导致的hash结构退化为链表(查询o(n)),树查询为o(logn)
为什么刚开始使用链表?树结构插入和删除更耗时,链表查询稍慢但节点操作快
为什么是红黑树不是AVL树 avl比红黑树查询快,但是删除和插入节点的代价更高
1.6版本concurrentHashmap实现
做到读取数据不加锁(volatile修饰table),并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。
ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
HashEntry
Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构:
1 2 3 4 5 6 | static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; } |
可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况。
Put方法实现
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
1.7 计算size的方式:分段计算两次,两次结果相同直接返回,否则对所有的分段加锁直接并且直接计算
1.8线程安全map实现
1.8版本相对1.6版本,摒弃了segment的思想,而是采用cas+syn的实现方式,底层数据结构与hashmap相同(数组 +链表、红黑树)。
Cas(compare and swap,乐观锁结构):比较内存中的值V和期望的老的值A,如果相等,则将修改后的值B赋给V;
hashTable 和 concurrentHashMap的rehash方式
hashTable两层for循环遍历,https://blog.csdn.net/valuetome111/article/details/77712933
chashMap 多线程并发rehash
concurrentHashMap的put流程
准备:value判空 数组为空则初始化数组 hash取余定位
执行:数组节点为空:直接CAS放入 不为空:是否在rehash,是的话加入,否则syn链表头后 遍历替换或插入
收尾:是否树化(长度大于8) 维护count等 是否扩容(装填因子)
concurrentHashMap怎么进行扩容的?
1准备: 计算线程数和每个线程负责的桶个数;初始化nextTable,扩容2倍
2 执行key value对的转移, 单个线程:
1)获取转移的桶区间
2)已经有了占位符,跳过
3)未被占位-有值:执行转移,将一个链表/树拆成两个,为0的放入原i位置,1的放入n+i位置
4)未被占位-空值,则进行占位(让putVal方法的线程感知)
https://www.cnblogs.com/stateis0/p/9062086.html
map对null的支持以及原因?
非并发的HashMap对象的key、value值均可为null。并发的HahTable\concurrentHashMap对象的key、value值均不可为null。
本质的区别是当你通过get(k)获取对应的value时,如果获取到的是null时,能否判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射?HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了.。
Collections.synchronizedMap(Map)
获取线程安全的map的又一种方式 使用Collections.synchronizedMap(Map), 默认将传入的map作为互斥锁,syn
为什么hashMap的key可以为空,而hashtable不可以?
因为Hashtable
key为null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断。
当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
他的原理是啥?
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。