ConcurrentHashMap源码分析

前言


ConcurrentHashMap是concurrent包里面出镜率很高的一个类,这个类是线程安全的Map,原来jdk1.8以前的ConcurrentHashMap采用的是锁分段机制来保证线程安全。

如果关注这个锁分段技术的,可以参考这篇博文:

http://blog.csdn.net/yansong_8686/article/details/50664351

至于我,可能就不详细说锁分段机制了,这篇文章主要针对的是JDK1.8中的ConcurrentHashMap的实现方法,锁分段技术,作为一种思想,我会简单说说我的理解,我也不知道全不全面,正不正确,也是一种理解吧。

先说说锁分段技术吧


先从一个问题说起,常常在面试中,都会问有关于HashMap的相关问题:HashTable和HashMap的区别是什么?

答案一般人可能会回答HashTable是线程安全的,HashMap不是线程安全的。

OK,这没错,那HashTable现在还在用吗?

不再用了。

为什么呢?

效率低。

效率怎么低了?

如果查看底层源码的时候,就会发现,它的多线程同步处理,简直简单粗暴。就是把所有的Map中的方法加上synchronized关键字。这样一来有什么问题呢?

这里写图片描述

一个线程正在做一个什么样的操作的话,其他的N个线程都要等待,这样的多线程在线程数非常大的情况下,根本就运转不开。那咋办?除了我们自己写,在jdk1.5之后, 出现的ConcurrentHashMap就提供的一种解决思路。

可以看到,下面这张图的话,就是1.8版本以前的分段加锁的机制, 这里的ConcurrentHashMap是分成了N个Segment,这个就是片段。每个Segment又有HashEntry的数组,数组又挂着链。因此,如果加锁的话,其实也就是加一个Segment的锁,对其他的Segment没有影响。这样的做法呢,实际上是把加锁的粒度变小了。锁还是加的,还是不是全加,加一部分。

这里写图片描述

JDK1.8有什么改变


从结构图上看,它放弃了原有的Segment的结构,直接用Entry数组的方式。

这里写图片描述

当然这个图,也不是特备完整,因为在JDK1.8中,当链表长度超过8的时候,链表会转换成红黑树。当然,这不在我们的ConcurrentHashMap的讨论范围之中,我们还是主要讨论线程安全怎么保证。

要探究,底层源码跑不掉

// 截取部分的ConcurrentHashMap的源码分析

// put方法的底层是putVal方法
public V put(K key, V value) {
    return putVal(key, value, false);
}

// 这个方法是不能被override
final V putVal(K key, V value, boolean onlyIfAbsent) {
        //  ConcurrentHashMap是不能put null的
        if (key == null || value == null) throw new NullPointerException();
        // 得到key对应的hash位置
        int hash = spread(key.hashCode());
        // 这个主要看到底一个index下挂的有几个Node,初始当然是没有Node
        int binCount = 0;
        // 无线循环
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 如果这个table都还没有初始化过
            if (tab == null || (n = tab.length) == 0)
                // 初始化table
                tab = initTable();
                // 如果index=hash%n的那个位置Node是空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 就把新的Node放到table的那个index处,如果是把新Node放到null的index处,不加锁。
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果取出的那个Node正在移动,那就帮助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 否则就是说呢hash为index的这个地方本来就有Node,到底是修改值,还是加链表
            else {
                V oldVal = null;
                // 在这个地方加了锁的操作,这个粒度是table中的某个index取出来的Node,粒度很小。
                synchronized (f) {
                    // 下面的这两个if就是再次判断的作用,是不是那个Node,是不是那个hash值
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            // 这个时候把binCount置为1,因为有一个Node嘛
                            binCount = 1;
                            // 这一步可以得出这个链表有多长
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果遍历的时候,找到了我们传入的key,已经在链表里有了
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    // value赋为新值,并且跳出循环
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                // 如果遍历完了还没找到
                                if ((e = e.next) == null) {
                                    // 把value变成链表的下一个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;
                            }
                        }
                    }
                }
                // 如果这个链表的Node的个数大于8了,转成红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 这个方法主要做了两件事,第一Map中的元素个数+1,第二判断需不需要扩容,如果需要,扩容。
        addCount(1L, binCount);
        return null;
    }


初始化table

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
     /**
     * SizCtl的解释
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */    
             // sc<0时,线程暂停,让CPU重新选择
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            // 如果>=0,看过我Atomic类介绍的亲是不是很熟悉这个方法,正式CAS方法实现同步
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // 再三确定table是空的
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 按照初始值16去创建Node数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 此时sc=12
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // sizeCtl=12,说明下次在size=12的时候,resize这个数组
                    sizeCtl = sc;
                }
                break;
            }
        }
        // 返回initial之后的table
        return tab;
    }

get方法

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算hash值
        int h = spread(key.hashCode());
        //根据hash值确定节点位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点  
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //如果eh<0 说明这个节点在树上 直接寻找
            else if (eh < 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;
    }

后记


这篇文章主要简单说了ConcurrentHashMap的安全机制,对size扩容机制并没有做详细的介绍,如果想进一步了解的朋友,可以看看参考文献中的文章,写的很好,受益良多。

参考文献


http://www.jianshu.com/p/e694f1e868ec
http://blog.csdn.net/u010723709/article/details/48007881
http://blog.csdn.net/yansong_8686/article/details/50664351

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值