concurrenthashmap源码分析 jdk7

8 篇文章 0 订阅

1.前言,分析当前问题

  不用说,concurrenthashmap就是为了解决map操作时并发问题,因为hashmap在多线程扩容的时候,扩容有个特征就是扩容一次后链表中的数据和原数据顺序是反的,比如在数组索引位子a的链表数据是1,2,3.扩容后数组a的链表数据就成了3,2,1.这个时候线程2并发的话,链表数据和next数据就是反的,就有可能形成死循环.

  hashtable可以解决这个问题,但是他是所有的方法都加上synchronized,虽然解决问题,但是影响性能,他会锁住整个数组,但是有时候如果并发不是在同一个索引位子,其实是没有问题的.

  concurrenthashmap使用的是分段锁的方式,hashtable锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table

2.concurrenthashmap源码分析

  先看结构图

从图中可以看到concurrenthashmap是由segement数组+hashEntry数组+链表组成

 

 

  2.1:构造方法源码

/**
     * Creates a new, empty map with the specified initial
     * capacity, load factor and concurrency level.
     *
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements.
     * @param loadFactor  the load factor threshold, used to control resizing.
     * Resizing may be performed when the average number of elements per
     * bin exceeds this threshold.
     * @param concurrencyLevel the estimated number of concurrently
     * updating threads. The implementation performs internal sizing
     * to try to accommodate this many threads.
     * @throws IllegalArgumentException if the initial capacity is
     * negative or the load factor or concurrencyLevel are
     * nonpositive.
     *
     * concurrencyLevel:并发级别
     * initialCapacity:segement数组下所有hashEntry数组的大小
     */
    @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        //concurrentHashMap中segement数组的大小
        int ssize = 1;
        //确保segment数组的大小是2的幂次方最右接近并发量的那个数
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //通过所有segment数组下所有hashEntry数组大小除以segment数组大小计算每个segment下hashEntry数组的大小
        int c = initialCapacity / ssize;
        //
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        //确保每个segement下hashEntry数组大小是2
        while (cap < c)
            cap <<= 1;
        // 创建segment对象,以后如果其他某个segment数组位子(除了第一个,第一个就是放的这个)需要创
//建segment就使用这个segment对象的结构参数来创建segment
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             //创建实际大小是所有segment下所有数组大小
//(也就是我们给的初始化大小)/并发级别后向上取整的大小.最小是2的hashEntry数组大小
                             (HashEntry<K,V>[])new HashEntry[cap]);
        //创建一个segment数组, 大小是2的幂次方最右接近并发量的那个数
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        //将创建的segment放在segment数组索引为0 的位子
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }




  2.2: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 <tt>get</tt> 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 <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>
     * @throws NullPointerException if the specified key or value is null
     */
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        //计算得到segment的索引.
            // segmentMask是segment数组大小-1
            // sshift是segment数组大小ssize由2的次幂计算得到的那个幂指数,比如ssize是16就是2的4次方,此时sshift就是4
            // segmentShift 是 32 - ssize
        int j = (hash >>> segmentShift) & segmentMask;
        // 安全的通过unsafe从segment数组获取到索引j位子的segment对象,如果为空
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            //创建segment数组j位子的segment对象
            s = ensureSegment(j);
        //已经有了segment对象,将key,value放到segment中
        return s.put(key, hash, value, false);
    }

安全的创建segement数组并放入到segement数组中,简单描述就是如果segement数组u位子为空,那么就是用segement数组第一个segement对象的一些基本信息(比如segement下的hashEntry数组长度,加载因子等)创建u位置的segement对象,然后使用自旋锁,使用unsafe的cas安全的将新建的segement对象放入到segement数组中.返回新建的segement对象

 /**
     * Returns the segment for the given index, creating it and
     * recording in segment table (via CAS) if not already present.
     * 通过unsafe的cas创建segment数组给定索引位子的segment对象,
     * 并放在segment数组该索引位子,返回创建的segement对象
     *
     * @param k the index
     * @return the segment
     */
    @SuppressWarnings("unchecked")
    private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        //计算segment的索引k的物理地址
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        //如果segment数组在u的位置没有segment对象,没有其他线程创建过segmen对象放在u索引位子
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //使用构造方法创建的第一个segment对象中的结构参数,比如segment内部hashentry数组大小,加载因子等
            // use segment 0 as prototype,使用segement数组的第一个segement对象作为样本,使用第一个segement对象的参数信息来进行当前u位子的segement对象的创建,
            //例如segement数组中u位子加载因子和u位子上segement下挂的hashEntry数组的大小等都是使用segement数组第一个segement对象的信息.
            Segment<K,V> proto = ss[0];
            // segement数组第一个位子下hashEntry数组长度
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            // 创建一个和segement数组第一个segement对象下的hashEntry数组大小一样的hashEntry数组.
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            //再次判断segment在u的位置是否已经有了segment对象,防止其他线程已经创建,现在重复创建
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                    == null) { // recheck
                //如果没有被创建,就自己创建一个segment对象
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                //自旋锁,使用unsaft的cas给segment数组u位子放上创建的segment对象.
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                        == null) {
                    // 如果u的位子是null,将创建的segement  s放入到segement数组ss中索引u的位子, seg不为空,结束循环.
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        //返回自己创建好或者别的线程创建好的segment对象.
        return seg;
    }
        /**
         * 遍历segment中的hashEntry数组
         * 如果有key和hash都相同的就将新的数据覆盖老数据,老数据返回回去,中断循环hashEntry数组
         * 如果一直循环到hashEntry数组最后e.next==null,
         *      就会创建一个新的hashEntry对象,使用头插法插入到hashEntry数组,然后将链表往下移动一位(类比).
         * 
         */
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //这个方式是segement的put方法,segment是继承ReentrantLock
            // 加锁,先用trylock再使用lock阻塞加锁,并且得到一个新创建的hashEntry对象
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //当前segment中的hashEntry数组.
                HashEntry<K,V>[] tab = table;
                //计算得到索引值
                int index = (tab.length - 1) & hash;
                //获取索引值的那个hashEntry对象.
                HashEntry<K,V> first = entryAt(tab, index);
                //遍历这个hashEntry数组tab
                for (HashEntry<K,V> e = first;;) {
                    //当前hashEntry不是空
                    if (e != null) {
                        K k;
                        //当前hashEntry的索引和传入的相同
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            //将当前hashEntry的旧值返回回去
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                //将新值赋值给当前的hashEntry
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        // 继续循环hashEntry数组tab,判断是否和传进来的key和hash是否一致.
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            //如果在scanAndLockForPut方法中创建过这个要put的hashEntry对象
                            //头插法放入到tab的hashEntry数组中
                            // 将第一个节点放到当前hashEntry对象后面,也就是头插法
                            node.setNext(first);
                        else {
                            //没有创建就创建好后,头插法放入到tab的hashEntry数组中
                            node = new HashEntry<K,V>(hash, key, value, first);
                        }
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            //如果segement下的hashEntry数量超过阈值,扩容,将旧的数据克隆到新的hashEntry数组中
                            rehash(node);
                        else
                            //将传入的数据放在index索引位,注意这里的index是hashEntry数组头结点的索引,
                            // 也就是说将新的hashEntry对象放在原来第一个节点的位子,
                            // 将原来第一个节点放在这个新的hashEntry后面,完成头插法后的往下移动位子,链表长度增加一个.
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //解锁
                unlock();
            }
            return oldValue;
        }
/**
*这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了     
*MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。
*这个方法就是看似复杂,但是其实就是创建了一个hashEntry对象并且获取该 segment 的独占锁
*
*
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node

    //循环获取锁
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node

         // 进到这里说明数组该位置的链表是空的,没有任何元素
         // 进到这里的另一个原因是 tryLock() 失败                    

node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {

        // 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
        //    lock() 是阻塞方法,直到获取锁后返回
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {

            // 这个时候就是有新的元素进到了链表,成为了新的表头

            // 这边的策略是,重新走一遍 scanAndLockForPut 方法
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

jdk8 hashmap的put方法源码

 /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;
        //当前hash对应的当前节点
        Node<K,V> p;
        int n, i;
        //将hashMap的node数组赋值给tab,判断当前hashMap为空就开始初始化或者两倍扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //获取到当前hash的node对象数据
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果为空,就创建当前node对象,赋值给tab数组的i(就是当前hash)位子.
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                //如果当前key或hash相同,就将当前节点赋值给新创建的node类型字段e
                e = p;
            else if (p instanceof TreeNode)
                //如果当前节点p是treeNode,就是用treeNode的put方法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //循环链表
                for (int binCount = 0; ; ++binCount) {
                    //当前hash的下个节点数据为空,也就是循环到了链表最后面,这里使用的是尾插法,这里就是和1.7不一样的地方
                    if ((e = p.next) == null) {
                        //创建该node节点,并赋值给p的next节点
                        p.next = newNode(hash, key, value, null);
                        //如果链表长度大于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //就将node数组下hash位子的链表下的所有node转成treeNode再转成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值