📫作者简介:小明Java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化,文章内容兼具广度、深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公司后端高级工程师。
🏆 2022博客之星TOP3 | CSDN博客专家 | 后端领域优质创作者 | CSDN内容合伙人
🏆 InfoQ(极客邦)签约作者、阿里云专家 | 签约博主、51CTO专家 | TOP红人、华为云享专家
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
本文目录
本文导读
一、ConcurrentHashMap底层实现(JDK1.8)
1、put() 方法源码
2、get() 方法源码
二、ConcurrentHashMap底层实现(JDK1.7)
三、ConcurrentHashMap的jdk7和8的区别
四、ConcurrentHashMap与Hashtable的区别
总结
本文导读
本文JDK1.8中ConcurrentHashMap底层实现以及put、get源码,对常见面试题ConcurrentHashMap的jdk7和8的区别、ConcurrentHashMap与Hashtable的区别进行讲解。
一、ConcurrentHashMap底层实现(JDK1.8)
底层数据结构:Node数组 + 红黑树
保证线程安全的方式:乐观锁 + Sysnchronized(1.8中的分段其实就是table数组中一个个的hash槽,这样使得添加节点时加锁粒度更小,并发度也更高)
Sysnchronized 锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁)并发扩容
读操作是无锁:Node 的 val 和 next 使用 volatile 修饰,读写线程对该变量互相可见,数组用volatile修饰,保证扩容时被读线程感知。
arduino/** * 每个 Node 里面是 key-value 的形式 * 并且把 value 用 volatile 修饰,以便保证可见性 * 同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构 */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; // ... }
为什么在有Synchronized 的情况下还要使用CAS
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能。
1、put() 方法源码
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) { throw new NullPointerException(); }
ini//计算 hash 值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K, V>[] tab = table; ; ) { Node<K, V> f; int n, i, fh; //如果数组是空的,就进行初始化 if (tab == null || (n = tab.length) == 0) { tab = initTable(); } // 找该 hash 值对应的数组下标 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果该位置是空的,就用 CAS 的方式放入新值 if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) { break; } } //hash值等于 MOVED 代表在扩容 else if ((fh = f.hash) == MOVED) { tab = helpTransfer(tab, f); } //槽点上是有值的情况 else { V oldVal = null; //用 synchronized 锁住当前槽点,保证并发安全 synchronized (f) { if (tabAt(tab, i) == f) { //如果是链表的形式 if (fh >= 0) { binCount = 1; //遍历链表 for (Node<K, V> e = f; ; ++binCount) { K ek; //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) { e.val = value; } break; } Node<K, V> pred = e; //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后 if ((e = e.next) == null) { pred.next = new Node<K, V>(hash, key, value, null); break; } } } //如果是红黑树的形式 else if (f instanceof TreeBin) { Node<K, V> p; binCount = 2; //调用 putTreeVal 方法往红黑树里增加数据 if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) { p.val = value; } } } } } if (binCount != 0) { //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8 if (binCount >= TREEIFY_THRESHOLD) { treeifyBin(tab, i); } //putVal 的返回是添加前的旧值,所以返回 oldVal if (oldVal != null) { return oldVal; } break; } } } addCount(1L, binCount); return null; }
2、get() 方法源码
kotlinpublic V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //计算 hash 值 int h = spread(key.hashCode()); //如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) { //判断头结点是否就是我们需要的节点,如果是则直接返回 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //如果头结点 hash 值小于 0,说明是红黑树或者正在扩容,就用对应的 find 方法来查找 else if (eh < 0) 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; }
二、ConcurrentHashMap底层实现(JDK1.7)
底层数据结构:Segment分段(数组+链表)
在jdk7中ConcurrentHashMap内部进行了Segment分段,Segment继承了ReentrantLock,各个Segment之间都是相互独立上锁的,互不影响。
每个Segment的底层数据结构与HashMap类似,是数组+链表。默认有0~15共16个Segment,所以最多可以同时支持16个线程并发操作(操作分别分布在不同的Segment上)。
16这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。
相比于之前的Hashtable每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。
三、ConcurrentHashMap的jdk7和8的区别
数据结构:Java7采用Segment分段锁来实现,Java8中的ConcurrentHashMap使用数组+链表+红黑树
并发度:Java7中,每个Segment独立加锁,最大并发个数就是Segment的个数,默认是16。Java8中,锁粒度是table数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。
原理:Java7采用Segment分段锁来保证安全,而Segment是继承自ReentrantLock。Java8中放弃了Segment的设计,采用Node+CAS+synchronized保证线程安全。
Hash冲突:Java7链表;Java8链表转换为红黑树,来提高查找效率。
查询时间复杂度:Java7遍历链表的时间复杂度是O(n),n为链表长度。Java8如果变成遍历红黑树,那么时间复杂度降低为O(log(n)),n为树的节点个数。
四、ConcurrentHashMap与Hashtable的区别
版本:ConcurrentHashMap(JDK1.5)与Hashtable(JDK1.0)
实现线程安全的方式不同:Hashtable的方法是被synchronized关键字修饰的。而ConcurrentHashMap利用了CAS+synchronized+Node节点的方式
性能不同:当线程数量增加的时候,Hashtable的性能会急剧下降,因为每一次修改都需要锁住整个对象,而其他线程在此期间是不能操作的,还会带来额外的上下文切换等开销,所以此时它的吞吐量甚至还不如单线程的情况。
在ConcurrentHashMap中,就算上锁也仅仅会对一部分上锁而不是全部都上锁,所以多线程中的吞吐量通常都会大于单线程的情况。
迭代时修改:Hashtable(包括HashMap)不允许在迭代期间修改内容,否则会抛出ConcurrentModificationException异常,其原理是检测modCount变量(当前Hashtable被修改的次数)。每一次去调用Hashtable的包括addEntry()、remove()、rehash()等方法中,都会修改modCount的值。迭代器在进行next的时候,也可以感知到,于是它就会发现modCount不等于expectedModCount。
总结
本文JDK1.8中ConcurrentHashMap底层实现以及put、get源码,对常见面试题ConcurrentHashMap的jdk7和8的区别、ConcurrentHashMap与Hashtable的区别进行讲解。