简介:以上文章讲述的是HashMap底层原理的实现接下来讲解的是HashTable,ConcurrentHashMap底层实现原理。觉得我还可以的可以加群探讨技术QQ群:1076570504 个人学习资料库http://www.aolanghs.com/ 微信公众号搜索【欢少的成长之路】
又到了整理技术点的时间了,今天讲述的是ConcurrentHashMap,大家对这个我相信也是很熟悉的,不知是否知道以下面试常问的一些技术点呢?
通过这篇文章你能学习到这些知识!想了解的继续深入,不想了解的赶紧离开,我不想浪费你们的学习时间。找准自己的定位与目标,坚持下去,并且一定要高效。我跟你们一样非常抵制垃圾文章,雷同文章,胡说八道的文章。
很多人会问,学底层多浪费时间,搞好实现功能不就好了吗?
可以这样想一下到了一定的工作年限技术的广度深度都有一定的造诣了,你写代码就这样了没办法优化了,机器配置也是最好的了,那还能优化啥? 底层,我们都知道所有语言到最后要运行都是变成机器语言的,最后归根究底都是要去跟机器交互的,那计算机的底层是不是最后还是要关注的东西了!
根据泊松分布,在负载因⼦默认为0.75的时候,单个hash槽内元素个数为8的概率⼩于百万分之⼀,所以将7作为⼀个分⽔岭,等于7的时候不转换,大于等于8的时候才进⾏转换,小于等于6的时候就化为链表。
处理方式
实现方式:
Tip: 这个时候很多小伙伴估计有点疑惑,为什么HashMap可以允许key,value为空而HashTable为啥不可以呢
1.因为Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理
2.Hashtable使⽤的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不⼀定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在存在进行判断,ConcurrentHashMap同理。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException(); !!!这就是为啥他不可以put null值的原因 !!!
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
Tip: 当其他线程改变了HashMap的结构,如:增删元素,将会抛出ConcurrentModificationException异常,而HashMap则不会。
fail-fast是啥
fail-fast原理是
fail-fast应用场景:
HashTable的性能比较低一些,并发度也不够。这个时候就用到了ConcurrentHashMap解决这个时候的并发问题。问题来了为什么并发那么高呢?
数据结构:
static final class Segment<K,V> extends ReentrantLock implements
Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 作⽤⼀样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 记得快速失败(fail—fast)么?
transient int modCount;
// ⼤⼩
transient int threshold;
// 负载因⼦
final float loadFactor;
}
为啥并发高:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍历该 HashEntry,如果不为空则判断传⼊的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 不为空则需要新建⼀个 HashEntry 并加⼊到 Segment 中,同时会先判断是否需要扩容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value,
first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
Tip:那get逻辑呢?
大概的1.7讲完了,那为啥1.7升级到1.8呢?说明肯定有它的优化之处!
1.7虽然可以⽀持每个Segment并发访问,但是还是存在⼀些问题=>因为基本上还是数组加链表的⽅式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的⼀样问题,所以他在jdk1.8完全优化了。
下面我们聊一下 值的存取操作么?以及是怎么保证线程安全的?
ConcurrentHashMap在进⾏put操作的还是⽐较复杂的,⼤致可以分为以下步骤:
ConcurrentHashMap的get操作⼜是怎么样⼦的呢?
public V get(Object key) {
int h = spread(key.hashCode());
ConcurrentHashMap.Node[] tab;
ConcurrentHashMap.Node e;
int n;
//根据计算出来的 hashcode寻址,如果就在桶上那么直接返回值
if ((tab = this.table) != null && (n = tab.length) > 0 && (e = tabAt(tab, n - 1 & h)) != null) {
int eh;
Object ek;
//如果是红黑树那就按照树的方式获取值
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || ek != null && key.equals(ek)) {
return e.val;
}
}
//还是不满足那就按照链表的方式遍历获取值
else if (eh < 0) {
ConcurrentHashMap.Node p;
return (p = e.find(h, key)) != null ? p.val : null;
}
while((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || ek != null && key.equals(ek))) {
return e.val;
}
}
}
return null;
}
CAS 是乐观锁的⼀种实现⽅式,是⼀种轻量级锁,JUC 中很多⼯具类的实现就是基于 CAS 的。
CAS 操作的流程如下图所示,线程在读取数据时不进⾏加锁,在准备写回数据时,⽐较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执⾏读取流程。这是⼀种乐观策略,认为并发操作并不总会发⽣
还是不明白?那我再说明下,乐观锁在实际开发场景中非常常见,⼤家还是要去理解
就⽐如我现在要修改数据库的⼀条数据,修改之前我先拿到他原来的值,然后在SQL⾥⾯还会加个判断,原来的值和我⼿上拿到的他的原来的值是否⼀样,⼀样我们就可以去修改了,不⼀样就证明被别的线程修改了你就return错误就好了。
举一个SQL的例子
//更新数据的时候 查询老数据是否等于我们查询的数据,
//如果是 说明没有被线程改过可以直接相加或者相减修改
update a set value = newValue where value = #{
oldValue}//oldValue就是我们执⾏前查询出来的值
CAS在一定程度上保证不了数据没被别的线程修改过!!!!!!!
比如ABA问题!什么是ABA?
一个线程把值改回了B,又来了⼀个线程把值⼜改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被⼈改过,其实很多场景如果只追求最后结果正确,这是没关系的
但是实际过程中还是需要记录修改过程的,比如资⾦修改什么的,你每次修改的都应该有记录,方便回溯
那怎么解决ABA问题?
⽤版本号去保证就好了,就⽐如说,我在修改前去查询他原来的值的时候再带⼀个版本号,每次判断就连值和版本号⼀起判断,判断成功就给版本号加1。(利用时间戳也可以解决ABA问题)
update a set value = newValue ,vision = vision + 1 where value = #
{
oldValue} and vision = #{
vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不⼀样
文章有些不足的地方希望指出,我会积极改正,提升我的技术!也希望我的文章可以帮助一下进阶程序员学到更多的东西。
小白一枚技术不到位,如有错误请纠正!最后祝愿广大的程序员开发项目的时候少遇到一些BUG。正在学习的小伙伴想跟作者一起探讨交流的请加下面QQ群。
知道的越多,不知道的就越多。找准方向,坚持自己的定位!加油向前不断前行,终会有柳暗花明的一天!
创作不易,你们的支持就是对我最大认可!
文章将持续更新,我们下期见!【下期将更新Redis底层实现原理 QQ群:1076570504 微信公众号搜索【欢少的成长之路】请多多支持!