设为首页 加入收藏

TOP

java基础系列之ConcurrentHashMap源码分析(基于jdk1.8)(一)
2019-08-26 06:53:30 】 浏览:71
Tags:java 基础 系列 ConcurrentHashMap 源码 分析 基于 jdk1.8

  1、前提

  在阅读这篇博客之前,希望你对HashMap已经是有所理解的,否则可以参考这篇博客: jdk1.8源码分析-hashMap;另外你对java的cas操作也是有一定了解的,因为在这个类中大量使用到了cas相关的操作来保证线程安全的。

  2、概述

  ConcurrentHashMap这个类在java.lang.current包中,这个包中的类都是线程安全的。ConcurrentHashMap底层存储数据的结构与1.8的HashMap是一样的,都是数组+链表(或红黑树)的结构。在日常的开发中,我们最长用到的键值对存储结构的是HashMap,但是我们知道,这个类是非线程安全的,在高并发的场景下,在进行put操作的时候有可能进入死循环从而使服务器的cpu使用率达到100%;sun公司因此也给出了与之对应的线程安全的类。在jdk1.5以前,使用的是HashTable,这个类为了保证线程安全,在每个类中都添加了synchronized关键字,而想而知在高并发的情景下相率是非常低下的。为了解决HashTable效率低下的问题,官网在jdk1.5后推出了ConcurrentHashMap来替代饱受诟病的HashTable。jdk1.5后ConcurrentHashMap使用了分段锁的技术。在整个数组中被分为多个segment,每次get,put,remove操作时就锁住目标元素所在的segment中,因此segment与segment之前是可以并发操作的,上述就是jdk1.5后实现线程安全的大致思想。但是,从描述中可以看出一个问题,就是如果出现比较机端的情况,所有的数据都集中在一个segment中的话,在并发的情况下相当于锁住了全表,这种情况下其实是和HashTable的效率出不多的,但总体来说相较于HashTable,效率还是有了很大的提升。jdk1.8后,ConcurrentHashMap摒弃了segment的思想,转而使用cas+synchronized组合的方式来实现并发下的线程安全的,这种实现方式比1.5的效率又有了比较大的提升。那么,它是如何整体提升效率的呢?见下文分析吧!

  3、重要成员变量

  1、ziseCtr:在多个方法中出现过这个变量,该变量主要是用来控制数组的初始化和扩容的,默认值为0,可以概括一下4种状态:

    a、sizeCtr=0:默认值;

    b、sizeCtr=-1:表示Map正在初始化中;

    c、sizeCtr=-N:表示正在有N-1个线程进行扩容操作;

    d、sizeCtr>0: 未初始化则表示初始化Map的大小,已初始化则表示下次进行扩容操作的阈值;

  2、table:用于存储链表或红黑数的数组,初始值为null,在第一次进行put操作的时候进行初始化,默认值为16;

  3、nextTable:在扩容时新生成的数组,其大小为当前table的2倍,用于存放table转移过来的值;

  4、Node:该类存储数据的核心,以key-value形式来存储;

  5、ForwardingNode:这是一个特殊Node节点,仅在进行扩容时用作占位符,表示当前位置已被移动或者为null,该node节点的hash值为-1;

  4、put操作

  先把源码摆上来:

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //key和value不能为空
        if (key == null || value == null) throw new NullPointerException();
        //通过key来计算获得hash值
        int hash = spread(key.hashCode());
        //用于计算数组位置上存放的node的节点数量
        //在put完成后会对这个参数判断是否需要转换成红黑树或链表
        int binCount = 0;
        //使用自旋的方式放入数据
        //这个过程是非阻塞的,放入失败会一直循环尝试,直至成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //第一次put操作,对数组进行初始化,实现懒加载
            if (tab == null || (n = tab.length) == 0)
                //初始化
                tab = initTable();
            //数组已初始化完成后
            //使用cas来获取插入元素所在的数组的下标的位置,该位置为空的话就直接放进去
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //hash=-1,表明该位置正在进行扩容操作,让当前线程也帮助该位置上的扩容,并发扩容提高扩容的速度
            else if ((fh = f.hash) == MOVED)
                //帮助扩容
                tab = helpTransfer(tab, f);
            //插入到该位置已有数据的节点上,即用hash冲突
            //在这里为保证线程安全,会对当前数组位置上的第一个节点进行加锁,因此其他位置上
            //仍然可以进行插入,这里就是jdk1.8相较于之前版本使用segment作为锁性能要高效的地方
            else {
                V oldVal = null;
                synchronized (f) {
                    //再一次判断f节点是否为第一个节点,防止其他线程已修改f节点
                    if (tabAt(tab, i) == f) {
                        //为链表
                        if (fh >= 0) {
                            binCount = 1;
                            //将节点放入链表中
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                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).putTreeva l(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //插入成功后判断插入数据所在位置上的节点数量,
                //如果数量达到了转化红黑树的阈值,则进行转换
                if
首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇在java中如何实现字符串的反转 下一篇在互联网中关系型数据库是否不再..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目