`

ConcurrentHashMap在jdk1.8和1.7中的区别

阅读更多

今天看了下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会开销更多的内存,虽然不是瓶             颈,但是也是一个选择依据 

 

分享到:
评论
1 楼 xinyun99 2019-05-17  
在jdk1.8中:

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作 都没看见Synchronized 怎么实现的

相关推荐

    java jdk1.7源码包,用于centos7使用jdk1.7编译openjdk1.8的 1.7版本

    Java JDK 1.7源码包是用于在CentOS 7操作系统上进行OpenJDK 1.8编译的重要资源。这个源码包包含了Java Development Kit的1.7版本,通常被称为JDK 7,它是Oracle公司发布的Java编程语言和Java平台标准版的一个实现。...

    ConcurrentHashMap的实现原理(JDK1.7和JDK1.8).pdf

    JDK1.8版本的`ConcurrentHashMap`进行了优化,参考了HashMap的改进,采用了数组+链表+红黑树的方式,并大量使用了CAS(Compare and Swap)操作,减少了锁的使用,进一步提高了并发性能。当链表长度超过一定阈值时,...

    java JDK1.8版本下载

    JDK1.8是Oracle公司发布的Java平台标准版(Java SE)的一个重要版本,它在2014年3月18日正式发布,引入了许多新的特性和改进。 **新特性** 1. **Lambda表达式**:这是JDK1.8最重要的特性之一,引入了函数式编程的...

    jdk1.8版本

    6. 并发改进:JDK1.8对并发库进行了优化,如Fork/Join框架的改进和ConcurrentHashMap的增强。 在安装PyCharm之前,确保正确安装JDK1.8的步骤如下: 1. 下载适用于Windows XP 32位系统的JDK1.8安装包。 2. 运行安装...

    涵盖了90%以上的面试题

    HashMap 不是线程安全的,如果在多线程环境中使用,应考虑使用 ConcurrentHashMap。 2. **HashTable**: HashTable 是线程安全的,但是效率较低,因为它对整个容器进行同步,导致在竞争激烈的场景下性能下降。 3. *...

    jdk1.8_64.7z

    jdk1.8 中对集合的底层结构做了调整。 如HashMap从1.7的数据+链表的形式调整为数据+链表+红黑树。 ConcurrentHashMap从分段机制+数组+链表+红黑树到CAS+数组+链表+红黑树。 这里先简要记录,后续会详解Map的原理与...

    jdk1.6和jdk1.7.rar

    本压缩包提供了两个版本的JDK,分别是JDK 1.6和JDK 1.7,适用于不同的操作系统平台。 JDK 1.6,也被称为Java SE 6,是Java平台的一个重要版本,发布于2006年。这个版本引入了许多新特性,提升了性能和开发者体验。...

    JDK 补丁包

    在给定的“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版本。它引入了许多新...

    Java 多线程与并发(13-26)-JUC集合- ConcurrentHashMap详解.pdf

    `ConcurrentHashMap`在JDK 1.7和1.8中有着显著的区别。 在JDK 1.7中,`ConcurrentHashMap`采用了分段锁(Segment)的设计思想,每个Segment是一个独立的可锁容器,类似于线程安全的`HashMap`。Segment的数量由`...

    java基础知识面试.pdf

    在面试中,Java后端开发人员经常被问到关于集合框架的问题,特别是关于List和Set的区别、HashSet的工作原理、HashMap的线程安全性以及JDK 1.7与JDK 1.8中HashMap的区别和优化。下面是这些知识点的详细解释: 1. ...

    蚂蚁面试题总结分享.doc

    但是在jdk1.8中,永久内存被移到了本地内存中,JVM内存结构变成了堆内存、栈内存和元空间(Metaspace)。元空间用于存放元数据信息,可以通过自增长来避免永久内存错误。 三、并发编程 并发编程是指应用交替执行...

    jdk-8u112-windows-x64.zip

    在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化 原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode...

    jdk1.8.0_60_linux.zip

    在jdk1.8中对hashMap等map集合的数据结构优化。hashMap数据结构的优化 原来的hashMap采用的数据结构是哈希表(数组+链表),hashMap默认大小是16,一个0-15索引的数组,如何往里面存储元素,首先调用元素的hashcode ...

    Java多线程-知识点梳理和总结-超详细-面试知识点.docx

    "Java多线程-知识点梳理和总结-超详细-面试知识点" Java多线程是Java编程语言中最基本也是最重要的概念之一。...同时,本文也讨论了JDK 1.7和JDK 1.8的差异,并提供了一些常见的Java多线程面试题。

    杭州蚂蚁金服Java高级工程师岗位面试真题

    - 在JDK1.8中,HashMap的数据结构进行了重大改进,引入了红黑树。这使得当链表长度超过8个时,链表会转换为红黑树,以减少查找时间。这种改变提高了Map在高负载下的性能。 2. **ConcurrentHashMap与HashMap的区别*...

    ArrayList集合与HashMap的扩容原来.docx

    2. ConcurrentHashMap,jdk1.7 添加了分段锁,就是在底层数组上,几个索引上添加一把锁,jdk1.8 添加的则是乐观锁,在每一个索引上都添加锁; 3. Conllections. synchronizedMap(map) 给 HashMap 整个集合添加了锁。...

    HashMap如何添加元素详解

    存储结构在jdk1.7当中是数组加链表的结构,在jdk1.8当中改为了数组加链表加红黑树的结构。 HashMap在多线程的环境下是不安全的,没有进行加锁措施,所以执行效率快。如果我么需要有一个线程安全的HashMap,可以使用...

    蚂蚁面试题总结.docx

    在Java的JDK 1.7到JDK 1.8中,Map接口的典型实现HashMap经历了重大的改进。在JDK 1.7中,HashMap采用的是简单的数组+链表的数据结构。每个元素存储在数组的一个槽位上,如果发生冲突,元素会通过链表链接起来。然而...

    蚂蚁面试题总结.doc

    - 在JDK1.8中,HashMap的数据结构发生了重大改变。从单纯的数组+链表结构转变为数组+链表+红黑树。这种变化使得在高并发环境下查找、插入和删除操作更加高效。 - 当链表长度超过8个节点时,链表会转换为红黑树,以...

Global site tag (gtag.js) - Google Analytics