今天看了下ConcurrentHashMap的实现源码,下面总结一下,只为自己加强记忆,如果想看详细讲解可以参考文章:https://blog.csdn.net/qq296398300/article/details/79074239
个人总结:
在jdk1.7中:
ConcurrentHashMap是由Segment数组和多个HashEntry数组组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样 。
put操作:对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置
static class Segment<K,V> extends ReentrantLock implements Serializable {
从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒.
get操作: ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null 。
size操作: 计算ConcurrentHashMap的元素大小是一个有趣的问题,因为他是并发操作的,就是在你计算size的时候,他还在并发的插入数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案 :
try { for (;;) { if (retries++ == RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation } sum = 0L; size = 0; overflow = false; for (int j = 0; j < segments.length; ++j) { Segment<K,V> seg = segmentAt(segments, j); if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0) overflow = true; } } if (sum == last) break; last = sum; } } finally { if (retries > RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) segmentAt(segments, j).unlock(); } }
1.第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的 2.第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。
在jdk1.8中:
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
ConcurrentHashMap的初始化其实是一个空实现,并没有做任何事,这里后面会讲到,这也是和其他的集合类有区别的地方,初始化操作并不是在构造函数实现的,而是在put操作中实现。
put操作:
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布 int binCount = 0; for (Node<K,V>[] tab = table;;) { //对这个table进行迭代 Node<K,V> f; int n, i, fh; //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED)//如果在进行扩容,则先进行扩容操作 tab = helpTransfer(tab, f); else { V oldVal = null; //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点 synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { //表示该节点是链表结构 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; //这里涉及到相同的key进行put就会覆盖原先的value 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; 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; //红黑树结构旋转插入 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount);//统计size,并且检查是否需要扩容 return null; }
这个put的过程很清晰,对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述
1.如果没有初始化就先调用initTable()方法来进行初始化过程
2.如果没有hash冲突就直接CAS插入
3.如果还在进行扩容操作就先进行扩容
4.如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
5.最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
6.如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
get操作:
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; int h = spread(key.hashCode()); //计算两次hash if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素 if ((eh = e.hash) == h) { //如果该节点就是首节点就返回 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来 //查找,查找到就返回 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
流程很简单,也很清晰,可以分为三个步骤来描述
1.计算hash值,定位到该table索引位置,如果是首节点符合就返回
2.如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
3.以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
总结与思考
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考
1.JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
2.JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了 3.JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
4.JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
1.因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中 ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优 势就没有了
2.JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的 关键字比使用API更加自然
3.在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶 颈,但是也是一个选择依据
相关推荐
Java JDK 1.7源码包是用于在CentOS 7操作系统上进行OpenJDK 1.8编译的重要资源。这个源码包包含了Java Development Kit的1.7版本,通常被称为JDK 7,它是Oracle公司发布的Java编程语言和Java平台标准版的一个实现。...
JDK1.8版本的`ConcurrentHashMap`进行了优化,参考了HashMap的改进,采用了数组+链表+红黑树的方式,并大量使用了CAS(Compare and Swap)操作,减少了锁的使用,进一步提高了并发性能。当链表长度超过一定阈值时,...
JDK1.8是Oracle公司发布的Java平台标准版(Java SE)的一个重要版本,它在2014年3月18日正式发布,引入了许多新的特性和改进。 **新特性** 1. **Lambda表达式**:这是JDK1.8最重要的特性之一,引入了函数式编程的...
6. 并发改进:JDK1.8对并发库进行了优化,如Fork/Join框架的改进和ConcurrentHashMap的增强。 在安装PyCharm之前,确保正确安装JDK1.8的步骤如下: 1. 下载适用于Windows XP 32位系统的JDK1.8安装包。 2. 运行安装...
HashMap 不是线程安全的,如果在多线程环境中使用,应考虑使用 ConcurrentHashMap。 2. **HashTable**: HashTable 是线程安全的,但是效率较低,因为它对整个容器进行同步,导致在竞争激烈的场景下性能下降。 3. *...
jdk1.8 中对集合的底层结构做了调整。 如HashMap从1.7的数据+链表的形式调整为数据+链表+红黑树。 ConcurrentHashMap从分段机制+数组+链表+红黑树到CAS+数组+链表+红黑树。 这里先简要记录,后续会详解Map的原理与...
本压缩包提供了两个版本的JDK,分别是JDK 1.6和JDK 1.7,适用于不同的操作系统平台。 JDK 1.6,也被称为Java SE 6,是Java平台的一个重要版本,发布于2006年。这个版本引入了许多新特性,提升了性能和开发者体验。...
在给定的“JDK 补丁包”中,我们重点关注的是JDK 1.7和1.8这两个版本的补丁。 1. JDK 1.7(Java SE 7) JDK 1.7,也称为Java SE 7 (Java Standard Edition 7),是Oracle在2011年发布的Java版本。它引入了许多新...
`ConcurrentHashMap`在JDK 1.7和1.8中有着显著的区别。 在JDK 1.7中,`ConcurrentHashMap`采用了分段锁(Segment)的设计思想,每个Segment是一个独立的可锁容器,类似于线程安全的`HashMap`。Segment的数量由`...
在面试中,Java后端开发人员经常被问到关于集合框架的问题,特别是关于List和Set的区别、HashSet的工作原理、HashMap的线程安全性以及JDK 1.7与JDK 1.8中HashMap的区别和优化。下面是这些知识点的详细解释: 1. ...
但是在jdk1.8中,永久内存被移到了本地内存中,JVM内存结构变成了堆内存、栈内存和元空间(Metaspace)。元空间用于存放元数据信息,可以通过自增长来避免永久内存错误。 三、并发编程 并发编程是指应用交替执行...
在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化 原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode...
在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化 原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode ...
"Java多线程-知识点梳理和总结-超详细-面试知识点" Java多线程是Java编程语言中最基本也是最重要的概念之一。...同时,本文也讨论了JDK 1.7和JDK 1.8的差异,并提供了一些常见的Java多线程面试题。
- 在JDK1.8中,HashMap的数据结构进行了重大改进,引入了红黑树。这使得当链表长度超过8个时,链表会转换为红黑树,以减少查找时间。这种改变提高了Map在高负载下的性能。 2. **ConcurrentHashMap与HashMap的区别*...
2. ConcurrentHashMap,jdk1.7 添加了分段锁,就是在底层数组上,几个索引上添加一把锁,jdk1.8 添加的则是乐观锁,在每一个索引上都添加锁; 3. Conllections. synchronizedMap(map) 给 HashMap 整个集合添加了锁。...
存储结构在jdk1.7当中是数组加链表的结构,在jdk1.8当中改为了数组加链表加红黑树的结构。 HashMap在多线程的环境下是不安全的,没有进行加锁措施,所以执行效率快。如果我么需要有一个线程安全的HashMap,可以使用...
在Java的JDK 1.7到JDK 1.8中,Map接口的典型实现HashMap经历了重大的改进。在JDK 1.7中,HashMap采用的是简单的数组+链表的数据结构。每个元素存储在数组的一个槽位上,如果发生冲突,元素会通过链表链接起来。然而...
- 在JDK1.8中,HashMap的数据结构发生了重大改变。从单纯的数组+链表结构转变为数组+链表+红黑树。这种变化使得在高并发环境下查找、插入和删除操作更加高效。 - 当链表长度超过8个节点时,链表会转换为红黑树,以...