java源码系列之初识ConcurrentHashMap(JDK1.8)

  • ConcurrentHahsMap简介

大家都知道HashMap是线程不安全的,在高并发的情况下可能会发生键值对丢失,迭代失败等等的问题,于是为了在高并发环境下使用HashMap,ConcurrentHashMap应运而生,看名字(并发的HashMap)就可以知道该容器适合在并发环境下使用。ConcurrentHashMap是在java并发包(java.util.concurrent)下的一个类,在并发包中有很多的并发容器以及创建线程池的工具类(实现了ExecutorServie接口),而ConcurrentHashMap也是属于并发容器的一种,下面我们先来看看这个类的结构图:

  • 类结构图

java源码系列之初识ConcurrentHashMap(JDK1.8)_第1张图片java源码系列之初识ConcurrentHashMap(JDK1.8)_第2张图片java源码系列之初识ConcurrentHashMap(JDK1.8)_第3张图片
由于Concurrent类的结构太过于复杂,这里只是选取了静态内部类,方法,静态常量各一部分方法,可以看到整个ConcurrentHashMap的设计和是实现是很负责且庞大的。

  • ConcurrentHashMap实现原理简述
    其实在java集合中还有一个类叫做HashTable,HashTable对每个方法都用了synchronized(相对重量级的锁)关键字修饰,以此来保证并发安全性,但是这样做的效率实在是不高,目前已经很少用到了,于是就有了ConcurrentHashMap,从Hashtable到ConcurrentHashMap,我们可以看到jdk的设计者为了追求更高的效率所做的努力,所以我们也要努力学习呀。
    从数据结构的角度来说,ConcurrentHashMap的实现和HashMap是一样的,都是利用了哈希表+红黑树来提供数据的存储服务,但是在ConcurrentHashMap中增加了一些设计来保证并发安全性。那么,为什么说ConcurrentHashMap是线程安全的呢?这是因为在ConcurrentHashMap中利用了分段锁的思想来提高ConcurrentHashMap的并发性。
    我们来分析一下,假如让你在HashMap的基础上来设计一种多线程安全的并发容器,你会怎么设计呢?一种常见的思路就是加锁,相信很多人都能够想到,但是这样和Hashtable有说明区别呢?都是加锁,只需要在get,put方法前面加一个synchronized关键字就可以了。其实再进一步思考一下,我们自己就可以想到类似的思想,既然在方法上加锁效率太低,那在方法内部加锁呢,即减小锁的粒度,实际上jdk源码中也确实是这样做的,在put方法内部加锁,我们来看一下put方法中的加锁的源码:后面会详细分析put方法
synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node 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 pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }

其实无论是jdk1.7还是jdk1.8中,都是利用了分段锁的思想来实现的,只不过实现的方式不同罢了,有兴趣的同学可以自己去看一下jdk1.7的源码。那么又为什么说是分段锁的思想呢?这是因为在put方法中,synchronized只是对操作涉及到的桶加锁了,并没有对整张哈希表进行加锁,这就是分段锁的具体实现了,我们看代码中synchronized加锁的对象f,这个f对象在前面被赋值为(f = tabAt(tab, i = (n - 1) & hash)看到这这行代码是不是很熟悉呢,没错,就是根据hash值来计算元素在数组中的下标,注意这里的hash值并不同于hashcode值,关于此处的具体实现会在在下面详细分析,下面我们先来分析这个容器的一些新增的属性(相较于HashMap而言)

  • 静态变量介绍
/**
     * Minimum number of rebinnings per transfer step. Ranges are
     * subdivided to allow multiple resizer threads.  This value
     * serves as a lower bound to avoid resizers encountering
     * excessive memory contention.  The value should be at least
     * DEFAULT_CAPACITY.
     */
    private static final int MIN_TRANSFER_STRIDE = 16;  //最小转移步长
/**
     * The maximum number of threads that can help resize.
     * Must fit in 32 - RESIZE_STAMP_BITS bits.
     */
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;  //进行扩容所允许的最大线程数
/**
     * The default concurrency level for this table. Unused but
     * defined for compatibility with previous versions of this class.
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;  //默认的并发等级,最多允许16个线程同时访问
  • put()方法分析
/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();  //ConcurrentHashMap中不允许key为null或者value为null
        int hash = spread(key.hashCode());  //计算key的hash值,计算方法与hashmap中不要太一样,加了一步操作,具体操作如下:return (h ^ (h >>> 16)) & HASH_BITS;
        int binCount = 0;
        for (Node[] tab = table;;) {  //进入无限循环,将表的引用赋给tab
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)  //如果表为空则初始化表,initTable()方法下面分析
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {  //否则计算key的数组下标并把该下标的引用赋值给f,如果为空则利用cas操作插入结点
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin  //当插入空bin中时不需要加锁,因为cas插入操作是原子性的
            }
            else if ((fh = f.hash) == MOVED)  //如果f的hash为MOVED = -1,表明当前表正在扩容,给fh复制为f.hash
                tab = helpTransfer(tab, f);  //让该线程进入协助扩容
            else {  //否则对数组对应下标下的链表进行加锁
                V oldVal = null;
                synchronized (f) {  //分段锁思想的代码实现,此处f已被复制并且不为null
                    if (tabAt(tab, i) == f) {  //判断f的引用是否发生了改变,即是否被其他线程篡改过了
                        if (fh >= 0) {  //如果fh>= 0表示当前表没有扩容
                            binCount = 1;  //binCount为1表示该链表没有被树化
                            for (Node e = f;; ++binCount) {  //无限循环遍历链表
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;   //如果key已经存在则保存旧值为oldVal
                                    if (!onlyIfAbsent)  //如果onlyIfAbsent为true,表示不允许覆盖,默认为false,表示允许覆盖旧值
                                        e.val = value;  //将value覆盖旧的value
                                    break;
                                }
                                Node pred = e;
                                if ((e = e.next) == null) {  //如果已经遍历至最后一个结点则创建一个新的结点插入,注意是尾插法
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {   //当前表没有进行扩容操作并且该数组下标所在的结点是一颗树结点,则进行红黑树的插入操作
                            Node p;
                            binCount = 2;  //binCount为2表示该链表已经被树化
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent) //此处操作的原因同上
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {  //binCount不为0表示链表或者红黑树发生了变化,需要进一步处理
                    if (binCount >= TREEIFY_THRESHOLD)  //如果插入后链表结点数大于默认值8则进行链表的树形化
                        treeifyBin(tab, i);
                    if (oldVal != null)  //oldVal为null说明存在相同的key,并且旧值已经被新值覆盖,返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);  
        return null;
    }

put()方法总结:
1.判断key或value是否为空,为空则抛出异常
2.计算key的hash值并进入无限循环
3.如果表为空则初始化哈希表
4.如果表已存在,计算key对应的数组下标,并判断该数组下标所在的结点是否为空,为空则利用cas操作插入新界点,注意此处仍然不需要加锁,因为cas操作保证了这一步是原子性的
5.判断MOVED == -1,为真说明当前表正在扩容,调用helptransfer()方法协助扩容
6.否则锁定该条链表进入无限循环遍历链表,这里是分段锁的具体实现
7.接下来的步骤和HashMap中的步骤类似,这里不再赘述,需要注意的一点是onlyIfAbsent参数在这里得到了使用,代码中的注释已经写的很清楚了,还有一点就是ConcurrentHashMap也是使用了尾插法插入结点,因为头插法可能会导致意外情况发生,例如jdk1.7HashMap在并发环境下的死循环
8.插入结束
关于put()方法涉及到的其他方法暂时先不解释,以后会更新。
本文仅代表个人理解,如有不当,欢迎指正!

你可能感兴趣的:(基础,java,java,并发容器)