• ConcurrentHashMap put get 源码解析


    ConcurrentHashMap

    public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
        implements ConcurrentMap<K,V>, Serializable {}

    可以看到,继承自AbstractMap, 实现了ConcurrentMap,以及可序列化接口。

    先看put方法:

        /**
         * Maps the specified key to the specified value in this table.
         * Neither the key nor the value can be null.
         *
         * <p>The value can be retrieved by calling the {@code get} method
         * with a key that is equal to the original key.
         *
         * @param key key with which the specified value is to be associated
         * @param value value to be associated with the specified key
         * @return the previous value associated with {@code key}, or
         *         {@code null} if there was no mapping for {@code key}
         * @throws NullPointerException if the specified key or value is null
         */
        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) {
            //key value 不允许为null
            if (key == null || value == null) throw new NullPointerException();
            //计算key的hash值
            int hash = spread(key.hashCode());
            //这个数值不知道用来干啥的
            int binCount = 0;
            for (Node<K,V>[] tab = table;;) {
                //f是当前key值获取到的桶的首元素,n是数组的长度,即桶数量,i是数组索引,fh是f的hash值
                Node<K,V> f; int n, i, fh;
                if (tab == null || (n = tab.length) == 0)
                    //初始化容器
                    tab = initTable();
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    //当table没有对应的索引桶,放入新数据的步骤,使用cas操作,可以实现无锁化
                    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)
                    //Helps transfer if a resize is in progress. 不知道干啥,先放着
                    tab = helpTransfer(tab, f);
                else {
                    //数组非空,并且对应索引桶也非空,遍历桶,以添加新节点或者覆盖旧节点
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                //node是普通的节点
                                binCount = 1;
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == hash &&
                                            ((ek = e.key) == key ||
                                                    (ek != null && key.equals(ek)))) {
                                        //hash值和key都相等,找到重复的键值
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    if ((e = e.next) == null) {
                                        //遍历到达链表的末尾,仍然没找到重复的键值,添加新的node
                                        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) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }

    通过查看put方法,大概了解其步骤如下:

      当数组为空时,初始化数组结构,初始化步骤包含了自旋(通过判断数组是否为空,以及是否允许初始化的状态位)以及cas操作,以保证只有一个线程能够初始化成功,防止多次初始化。

      数组初始化成功后,计算key值的hash,通过hash对数组长度求余,获取数组索引值,通过索引获取数组的桶的首节点数据

      如果桶首元素为空,通过cas操作对空桶新增首元素。这里也是通过自旋,以及cas操作保证线程安全性,只有一个线程能够对空桶新增首元素。

      如果桶首元素不为空,并且数组正在transfer中(我的理解transfer是重构数组结构中,这个可以通过节点的hash值来判断,负数的hash值有特殊含义),则帮忙transfer(这个步骤,初步理解是这样的:transfer操作比较耗时,其他线程的帮忙,可以加快速度)

      如果桶首元素不为空,数组没有transfer,则锁定(synchronized)桶首元素,执行以下步骤

        如果桶数据结构为普通链表,遍历链表,根据遍历结果,新增节点或者覆盖旧节点

        如果桶数据结构为红黑树,遍历红黑树,根据遍历结果,新增节点或者覆盖旧节点

      最后判断新增节点后,是否需要将对应桶的链表(如果是链表的话)结构转换成红黑树,然后将旧元素的节点数据的value值返回(没有旧元素则返回null)

      

    其中,计算key的hash值的方法如下:

    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    
    static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }

    而HashMap的计算Hash值的方法如下:

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    区别如下:

      前者(ConcurrentHashMap)不需要考虑key为null的情况,因为不允许key和value为null。

      前者比后者计算多出来一步,即 “&HASH_BITS” 的操作 ,该操作的作用是把最高位(即符号位)设置为0。

    对比jdk1.7和jdk1.8的实现,1.8弃用了1.7的Segment的概念,也就是分段锁。

    1.7的分段锁Sement继承了Lock类,可以对每个Segment进行锁定,而其实每个Segment就是数组中的多个桶的集合,其锁定粒度,既不会太大,也不会太小,相对于HashTable的直接锁定整个数组而言,是一种优化。在1.8中被放弃的原因,个人理解如下:

      jdk1.8中,ConcurrentHashMap用synchronized和cas取代Lock来保证线程安全。cas不用说,只要竞争不太激烈,是一种较高效的无锁化机制,然后Synchronized是jdk内置的关键字,JVM可以对其进行优化,而Lock类对象则不行。这里synchronized的优化涉及到锁升级,锁消除等。从轻量到重量,分为:偏向锁(无锁化)、轻量级锁(CAS,自旋),重量级锁(竞争的线程进入阻塞状态。并且操作系统在加锁和释放锁过程,需要在用户态和核心态之间进行切换,因为该操作依赖于操作系统的互斥锁,开销很大。当前线程释放锁后,需要从阻塞队列的对头拿到下一个线程,并进行唤醒操作)。

      1.8中,是对数组的每个桶进行加锁,相比Segment,锁粒度更小,并发度更高了。

    get函数如下:

    /**
         * Returns the value to which the specified key is mapped,
         * or {@code null} if this map contains no mapping for the key.
         *
         * <p>More formally, if this map contains a mapping from a key
         * {@code k} to a value {@code v} such that {@code key.equals(k)},
         * then this method returns {@code v}; otherwise it returns
         * {@code null}.  (There can be at most one such mapping.)
         *
         * @throws NullPointerException if the specified key is null
         */
        public V get(Object key) {
            
            //tab是数组,e是索引桶的首节点,p是,n是数组长度,eh是e的hash值,ek是e的key
            Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
            //求key的hash值
            int h = spread(key.hashCode());
            
            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;
                }
                else if (eh < 0)
                    //hash值小于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;
        }

    源码对应java版本:

    >java -version
    java version "1.8.0_102"
    Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
    Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)

  • 相关阅读:
    Struts tags--Data tags
    Java NIO学习笔记七 Non-blocking Server
    Java NIO学习笔记六 SocketChannel 和 ServerSocketChannel
    Java NIO学习笔记五 FileChannel(文件通道)
    Java NIO学习笔记四 NIO选择器
    Java NIO学习笔记 三 散点/收集 和频道转换
    SpringMVC接收集合页面参数
    JAVA NIO学习笔记二 频道和缓冲区
    Java NIO学习笔记一 Java NIO概述
    通过举例了解java中的流
  • 原文地址:https://www.cnblogs.com/zhangxuezhi/p/11871927.html
Copyright © 2020-2023  润新知