资讯专栏INFORMATION COLUMN

JDK1.8下ConcurrentHashMap的一些理解(一)

Andrman / 379人阅读

摘要:如下代码省略相关代码省略相关代码可以看到在里面,是直接采用数组链表红黑树来实现,时间复杂度在和之间,如果链表转化为红黑树了,那么就是到。

在JDK1.8里面,ConcurrentHashMap在put方法里面已经将分段锁移除了,转而是CAS锁和synchronized

ConcurrentHashMap是Java里面同时兼顾性能和线程安全的一个键值对集合,同属于键值对的集合还有HashTable以及HashMap
HashTable是一个线程安全的类,因为它的所有public方法都被synchronized修饰,这样就导致了一个问题,就是效率太低。

虽然HashMapJDK1.8的并发场景下触发扩容时不会出现成环了,但是会出现数据丢失的情况。
所以如果需要在多线程的情况下(多读少写))使用Map集合的话,ConcurrentHashMap是一个不错的选择。

ConcurrentHashMap在JDK1.8的时候将put()方法中的分段锁Segment移除,转而采用一种CAS锁和synchronized来实现插入方法的线程安全。
如下代码:

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
       //省略相关代码
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node(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;
                synchronized (f) {
                    //省略相关代码
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

可以看到在JDK1.8里面,ConcurrentHashMap是直接采用数组+链表+红黑树来实现,时间复杂度在O(1)和O(n)之间,如果链表转化为红黑树了,那么就是O(1)到O(nlogn)。
在这里值得一提的是,ConcurrentHashMap会判断tabAt(tab, i = (n - 1) & hash)是不是 null,是的话就直接采用CAS进行插入,而如果不为空的话,则是synchronized锁住当前Node的首节点,这是因为当该Node不为空的时候,证明了此时出现了Hash碰撞,就会涉及到链表的尾节点新增或者红黑树的节点新增以及红黑树的平衡,这些操作自然都是非原子性的。

从而导致无法使用CAS,当Node的当前下标为null的时候,由于只是涉及数组的新增,所以用CAS即可。

因为CAS是一种基于版本控制的方式来实现,而碰撞之后的操作太多,所以直接用synchronized比较合适。
ConcurrentHashMap在迭代时和HashMap的区别

当一个集合在迭代的时候如果动态的添加或者删除元素,那么就会抛出Concurrentmodificationexception,但是在ConcurrentHashMap里面却不会,例如如下代码:

public static void main(String[] args) {
    Map map = new ConcurrentHashMap();
    map.put("a","a1");
    map.put("b","b1");
    map.put("c","c1");
    map.put("d","d1");
    map.put("e","e1");
    Iterator iterator = map.keySet().iterator();
    while (iterator.hasNext()){
        String it = iterator.next();
        if("b".equals(it)){
            map.remove("d");
        }
        System.out.println(it);
    }
}

控制台打印如下:
a
b
c
e

而当你把ConcurrentHashMap换成HashMap的时候,控制台就会抛出一个异常:

Exception in thread "main" a
b
java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
    at java.util.HashMap$KeyIterator.next(HashMap.java:1466)
    at xyz.somersames.ListTest.main(ListTest.java:22)

原因在于ConcurrentHashMapnext方法并不会去检查modCountexpectedModCount,但是会检查下一个节点是不是为空

  if ((p = next) == null)
    throw new NoSuchElementException();

当我们进行remove的时候,ConcurrentHashMap会直接通过修改指针的方式来进行移除操作,同样的,也会锁住数组的头节点直至移除结束,所以在同一个时刻,只会有一个线程对当前数组下标的所有节点进行操作。

但是在HashMap里面,next方法会进行一个check,而remove操作会修改modCount,导致modCountexpectedModCount不相等,所以就会导致
ConcurrentModificationException

稍微修改下代码:

public static void main(String[] args) {
    Map map = new ConcurrentHashMap();
    map.put("a","a1");
    map.put("b","b1");
    map.put("c","c1");
    map.put("d","d1");
    map.put("e","e1");
    Iterator iterator = map.keySet().iterator();
    while (iterator.hasNext()){
        if("b".equals(iterator.next())){
            map.remove("d");
        }
        System.out.println(iterator.next());
    }
}
控制台打印如下:
b
d
Exception in thread "main" java.util.NoSuchElementException
    at java.util.concurrent.ConcurrentHashMap$KeyIterator.next(ConcurrentHashMap.java:3416)
    at com.xzh.ssmtest.ListTest.main(ListTest.java:25)
并发下的处理

由于每一个Node的首节点都会被synchronized修饰,从而将一个元素的新增转化为一个原子操作,同时Nodevaluenext都是由volatile关键字进行修饰,从而可以保证可见性。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74650.html

相关文章

  • ConcurrentHashMap基于JDK1.8源码剖析

    摘要:下面我来简单总结一下的核心要点底层结构是散列表数组链表红黑树,这一点和是一样的。是将所有的方法进行同步,效率低下。而作为一个高并发的容器,它是通过部分锁定算法来进行实现线程安全的。 前言 声明,本文用的是jdk1.8 前面章节回顾: Collection总览 List集合就这么简单【源码剖析】 Map集合、散列表、红黑树介绍 HashMap就是这么简单【源码剖析】 LinkedHas...

    sanyang 评论0 收藏0
  • 趣谈ConcurrentHashMap

    摘要:最近准备面试,一谈到基础,大部分面试官上来就数据结构素质三连与区别,底层数据结构,为什么能保证线程安全。数组顺序存储,内存连续,查询快,插入删除效率稍微低,不过现在略有改善。而在开始,是由和的方式去实现高并发下的线程安全。 最近准备面试,一谈到java基础,大部分面试官上来就java数据结构素质三连:ArrayList与LinkedList区别,HashMap底层数据结构,Concur...

    Travis 评论0 收藏0
  • 这几道Java集合框架面试题在面试中几乎必问

    摘要:若遇到哈希冲突,则将冲突的值加到链表中即可。之后相比于之前的版本,之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值默认为时,将链表转化为红黑树,以减少搜索时间。有序,唯一红黑树自平衡的排序二叉树。 本文是最最最常见Java面试题总结系列第三周的文章。主要内容: Arraylist 与 LinkedList 异同 ArrayList 与 Vector 区别 HashMap的底层...

    bigdevil_s 评论0 收藏0
  • ConcurrentHashMap源码分析_JDK1.8版本

    ConcurrentHashMap源码分析_JDK1.8版本 声明 文章均为本人技术笔记,转载请注明出处[1] https://segmentfault.com/u/yzwall[2] blog.csdn.net/j_dark/ JDK1.6版本 ConcurrentHashMap结构 showImg(https://segmentfault.com/img/remote/146000000900...

    animabear 评论0 收藏0

发表评论

0条评论

Andrman

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<