# 源码解析HashTable、HashMap、ConcurrentHashMap

源码解析HashTable、HashMap、ConcurrentHashMap


先上一张Java集合框架图1
这里写图片描述
查看大图


下面我们从HashTable开始,深入源码,分析HashTable、HashMap、ConcurrentHashMap这三个容器的区别。

HashTable

继承和实现
public class Hashtable<K,V>zz
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable 

可以看到Hashtable继承了Dictionary字典,用于存储

boolean containsKey​(Object key); 
V get​(Object key);
V put​(K key, V value);
V remove(Object key);
int size();
boolean isEmpty();

更多的抽象方法请查看API-Map

成员变量
    private transient Entry<?,?>[] table;
    private transient int count;
    private int threshold;
    private float loadFactor;
    private transient int modCount = 0;

我们可以看到变量中有个修饰符:transient,使用transient修饰的变量当对象存储时,它的值不需要维持。通俗一点说,transient关键字标记的变量不参与序列号过程


Entry

   /**
     * Hashtable bucket collision list entry
     */
    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }
        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,(next==null ? null : (Entry<K,V>) next.clone()));
        }
        /*
        省略get/set方法
        */
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }
hash:存储当前对象的hash散列值
key、value:用于存储对象的key和value值,并且key声明为final,表示对象的key赋值后不可修改
Entry<K,V> next:说明Entry是一个链表,next用来连接下一个Entry所在的地址

count 存储当前HashTable的Entry对象的数量


关于 threshold、loadFactor以及capacity这三个值,threshold、loadFactor是两个成员变量值,而capacity是HashTable对象初始化时,构造函数的一个输入参数。
threshold,表示当前HashTable能够存储的最大数量,如果当前对象数量>=threshold,则Hashtable就会ReHash,对Hashtable进行扩容。
Rehash的具体内容请见后文。
threshold = capacity * loadFactor。loadFactor又叫负载因子,指当前HashTable的使用率,默认的值为0.75,这个值不能太大也不能太小,如果太大,则HashTable过满,某一个散列值槽位的链表可能会过长,导致查询的效率降低;如果太小,则HashTable的利用率就会变低,浪费了空间资源。
capacity是值Hashtable的大小,在创建HashTable时,如果调用下面的构造函数,则输入一个HashTable的初始大小。

public Hashtable(int initialCapacity, float loadFactor);
public Hashtable(int initialCapacity);

如果调用无参数的构造方法

public Hashtable() {
        this(11, 0.75f);
    }

capacity的值默认为11

Hashtable(int initialCapacity)和Hashtable() 都会通过this(initialCapacity, loadFactor)调用下面的构造方法
默认loadFactor为0.75

    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

如果initialCapacity小于0则返回不合法初始异常,如果initialCapacity等于0,则初始initialCapacity为1
其他情况则初始化Entry数组,且赋值threshold
这里有一个MAX_ARRAY_SIZE的定义如下:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

modCount = 0 值当前HashTable 修改的次数(指HashTable长度的变化修改),用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)。

方法函数

在HashTable中,基本所有的方法都由synchronized关键字修饰,来解决并发问题,这也是HashTable性能低下的根本原因。至于synchronized关键字的原理,请见csdn

public synchronized int size() {
        return count;
}

实现的是接口Map

public synchronized boolean isEmpty() {
        return count == 0;
    }

实现的是接口Map

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

get方式也是实现的Map中的get方法,通过key值,查询到key值对于的value,如果当前对象不存在,就返回null
方法中,首先将当前的散列表赋值给新的tab,然后获取当前key的hash值,然后对hash值对tab数组的长度进行取余运算,得到当前所在的数组下标,并对当前槽位的链表进行遍历,找到hash值相同且key值相等
这里需要注意:hash值相同但key值不一定相等
hash & 0x7FFFFFFF 保证了得到的索引值是一个正数
这里通过先比较hash值,来提高查找的效率(hash值的获取比对象的获取更加的高效)

put方法

 public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

通过实现Map

 private void addEntry(int hash, K key, V value, int index) {
        modCount++;
        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }
        // Creates the new entry.
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

首先将modCount的值加1,这里判断了一下table是否需要扩容,如果需要扩容就进行rehash()扩容,并重新获取相应的table、hash以及index数组下标
最后将通过给tab[index]赋予新的值,将新的Entry对象添加到链表头,并将count++
可以发现,HashTable存储的key和value都是不能为空的


HashTable的rehash
这里讲到了put方法中,如果当前Table的对象数量达到了threshold,就会进行rehash。rehash的原理也比较简单,就是新建一个table且容量扩大两倍+1,并将数组中对应的链表迁移到新的table中

    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

可以看到扩容是将初始容量进行移位操作,扩大两倍+1,创建一个新的Entry数组,将threshold重新初始化,并将新的Map数组赋值给table,最后遍历所有的Entry对象,迁移到新的table上。结果如图所示:
这里写图片描述
这里需要注意的一个地方是:
扩容后的链表和原链表的顺序是相反的


remove

   public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        Entry<K,V> e = (Entry<K,V>)tab[index];
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

这里的remove操作就是找到key对于的Entry对象并将此对象的前一个Entry指向后一个Entry,并返回删除的Entry对象


HashMap

继承和实现
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

可以发现在接口实现上,相比HashTable是相同的,只是在继承上,HashMap继承的是AbstractMap,查看API文档可以知道,AbstractMap是一个抽象类,实现了Map

 static class Node<K,V> implements Map.Entry<K,V> 

Entry的实现上来看,最大的区别就在于hashCode的函数实现


HashTable中:

 public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }

HashMap中:

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

由此可以看出,HashMap中,key和value的值可以是null。并且当key为空时,获取到的hash值为0,表示当前对象放置在第一个槽位。HashMap也引入了高位运算>>>逻辑右移,,将低十六位和高十六位进行异或,尽量减少hash冲突


讲到Hash冲突,这里着重讲一下HashMap在Hash运算的两点高明的改进之处:

  • 一、 计算hashcode时引入高16位与低十六进行运算,在我们利用hashcode计算index时,我们知道是用(n-1)& hash 来得到下标,我们知道HashMap中 table的长度都是2的n次幂(详情请见下面的构造函数差别),因此index仅与hash值的低n位相关,假如table的length为 16=24 16 = 2 4 ,则index仅与低4位有关,很容易出现hash冲突。引入高位能够尽量的避免这样的情况出现
  • 二、 在计算threshold时,利用下面这个函数。
    我们在HashTable中了解到,threshold是initialCapacity*loadFactory。threshold是一个阈值,判断当前是否应该扩容。而在HashMap中,利用tableSizeFor函数,来进行threshold的赋值,构造函数初始化时没有用到loadFactory,这是因为构造函数没有初始化table,当调用put方法时才初始化table,相当于延迟初始化的方法(后面讲resize扩容时会讲到)。
    下面我先讲一下这个算法的原理。
    我们知道,table的初始长度capacity肯定是2的幂,因此,这个算法主要用于找到大于等于initialCapacity的最小的2的幂,如果等于则就返回这个数。首先我们看到将 int = cap-1,这是为了防止当cap已经是2的幂次方时,没有执行减1操作,则执行完这个算法时,得到的将会是一个cap的两倍。
    如果n为0,则经过几次无符号右移依然是0,最后返回的是capacity是1(最后有一个n+1操作)
    下面讲的是n不为0的情况
    第一次右移: n |= n >>> 1;
    因为在n不为0,始终有一位为1。首先进行向右移位,则最高为1的位置向右移动1位;然后将n与移位后进行或运算,则得到的结果为,最高为1的位置和其右边的一位也为1。则就有连续的两个1
    第二次右移: n |= n >>> 2;
    进过第一次运算后,最高位两个为1的位置是连续的,通过右移两位,再进行或运算,则得到最高连续4位为1
    第三次右移: n |= n >>> 4;
    第三次右移后,得到最高位后连续八位为1,后面的以此计算。
    因为容量最大为32位,当32位全为1后,最大容量为:MAXIMUM_CAPACITY。

提醒两点:
1、这有一个规律,任何不为2幂次方的数,他的最高位是1的位置始终是大于等于它为2的幂次方的数的最高位为1的后面一位。可能这句话有点绕口,我举个例子:假如为3,它的二进制位0011,比它大且为2的幂次的数为4,二进制为0100,刚好比3的最高为1的位置多了一位。
2、尽管每次运算都要执行到右移16位,但是当一个数的最高为1的位置不足16位或者8位或者4位时,后面高的移位操作且求或运算后,这个数依旧不变
3、最后得到的数就是将它最开始的最高为1的位右边的所有位全部变为1,最后执行一个+1操作就可以得到大于等于它的最小为2的幂次方的数。不得不说此算法的高明之处!


//静态函数
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
//构造函数中调用:
this.threshold = tableSizeFor(initialCapacity);

举一个例子:

这里写图片描述

成员变量

HashMap中的主要成员变量和HashTable相同:threshold,loadFactor,size,modCount。前面已经说过了这些变量的含义,这里就不再赘述。


方法函数

构造函数
同HashTable,HashMap的构造函数也有4个,具体请参照上文HashTable,这里说明一下其中的细小区别,其他不再赘述。

this.threshold = tableSizeFor(initialCapacity);

这里将tableSizeFor得到大于等于initialCapacity的最小2的幂的整数赋值给threshold,而不是通过capacity*loadFactory。在讲算法的时候已经讲过,这里并没有初始化table,而是在调用put方法时,判断为空,在resize中进行初始化。后面讲put方法和扩容时会讲到


get函数

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

可以看到这里调用的是getNode函数进行查找
再看getNode()函数源码:

 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        /*
         *判断当前table是否是空,如果为空直接返回null
         *当tab不为空,tab.length>0,且tab对应槽位的链表也不为空时,进行查找
         */
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {

            //判断第一个Node是否是要查找的对象
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果链表头结点的next不为空则继续遍历查找
            if ((e = first.next) != null) {

                //如果头结点为一个树的根节点,则进行树的遍历
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //如果不为树的根节点,则遍历链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

这个函数有一点需要注意:
当头结点不为空且头结点的next也不为空时,这里有一个分支判断:如果头结点是树节点,则进行树的遍历,如果不是,则进行链表的遍历。在JDK 1.7以前,槽位全部使用的是链表,而JDK1.8以后,加入了红黑树(对于红黑树的原理,后面会有相关的文章,此时请自己查找相关资料),此时只需要知道红黑树是一种特殊的自平衡二叉树就行。对于查找来说,红黑树的效率会高很多。毕竟使用过二叉树的都应该知道,二分法查找。在JDK 1.8以后,当某一个槽位的节点数量大于8时,使用红黑树。当小于或等于6时,使用链表。此时有一个神奇的数量:7,当进行put和remove使数量在7左右变换的时候,效率将会非常低,其中的问题请自主思考!这里就不展开说明红黑树了。

put函数


public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

这里可以看到在put中调用了putVal进行插入操作,下面我们看putVal的源码

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //首先判断tab是否没有初始化:为空
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果没有初始化,则调用resize进行初始化,并返回初始化后tab的length
            n = (tab = resize()).length;

            //同样根据hash值找到对应index的槽位是否为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果为空,则直接新建一个Node并赋值给对应的槽位
            tab[i] = newNode(hash, key, value, null);
        else {
          //如果不为空
            Node<K,V> e; K k;
            //判断第一个头结点是否为要插入的Node对象
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果头结点和要插入的对象不同,判断头结点是否是树节点
            else if (p instanceof TreeNode)
                //如果是树节点,则将对应的节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //如果头结点不是树节点,则将对应的Node节点插入链表中
            //遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果next为空,则直接将新的Node插入到尾结点后
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);

                        //判断当前节点是否大于八个(新插入了一个节点),如果是则将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果新节点存在,则直接退出查找并返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e不为空,则表示要插入的节点已经存在
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //插入后将size+1 并且判断当前的size是否大于threshold阈值,大于就进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

这里有一点需要说明:
afterNodeAccess(e)、afterNodeInsertion(evict)在HashMap中的函数体为空,是一个用于LinkedHashMap的回调函数,在HashMap中没有用处。在LinkedHashMap中,将这些函数进行实现。


resize
前面在put和get方法中都用到了resize,就是HashMap的扩容操作。下面将会具体讲述HashMap中resize的原理和其与HashTable中的不同之处
在分析resize源码之前,我们根据前面所了解的知识,可以粗略总结一下resize需要完成的工作有以下几点:

  • 当put Node对象节点时,会判断当前的table是否为空,如果为空就会进行resize()完成table的初始化操作
  • 当插入一个节点后,如果当前HashMap的Node数量count>threshold,就会通过resize()进行扩容
  • 对table进行扩容后,将原来对于槽位的数据迁移到新的table数组上
  • 重新计算threshold

下面来看resize的源码

final Node<K,V>[] resize() {
        //将需要扩容的table,赋值给新的table上
        Node<K,V>[] oldTab = table;
        //判断当前的table是否为空,如果为空,则当前的数组长度设置为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //将当前的阈值赋值给oldThr
        int oldThr = threshold;
        //初始化新的数组长度和threshold
        int newCap, newThr = 0;
        //如果table不为空,则table长度>0
        if (oldCap > 0) {
             //判断当前的table数组长度是否超过了最大(默认为2的30次方)
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果超过就将阈值设置为int所能表示的最大长度2的31次方,并返回oldTab。不扩容,只修改阈值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }//将table的长度扩大为2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 如果table长度大于默认的初始长度16,设置阈值为原来的两倍
        }//当table为空,且阈值大于0
        else if (oldThr > 0) // 新的初始化table长度设置为阈值大小
            newCap = oldThr;
        else {  //当table为空且阈值也小于0,即调用了无参数的构造函数。(仅设置了负载因子为默认的0.75)
            newCap = DEFAULT_INITIAL_CAPACITY;   //table的容量设置为默认的16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  //新的阈值为16*0.75
        }
        /*进入下面这个if语句的条件是:
         *当创建HashMap的时候输入了一个小于16的初始大小,前面虽然将长度扩大两倍,但是不满足大于16的要求
         *或者是第一次扩容初始化Table,且构造函数传入了一个初始容量
         *此时newThr为0
        */
        if (newThr == 0) {
            //将newThr设置为初始化长度*负载因子
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将newThr赋值给阈值,并用新的长度(扩大两倍后),创建新的newtable,并将新的newtable赋值给table 
        threshold = newThr;
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
       /*
       * 后面就是通过遍历将原来的节点通过hash迁移到新的table上,返回新的table。
       * 省略迁移的代码,详情请查看hashTable的迁移,
       * 这里多一点就是只是需要判断一下是否为红黑树头结点
       */

        return newTab;
    }

resize需要注意两点:
要分清楚扩容分三种情况:

  • table不为空:分两种情况:table的长度小于16和table的长度大于16(HashMap不是第一次执行resize)
  • table为空,但是阈值不为空(HashTable调用单参数或双参数构造函数创建,第一次执行resize)
  • table为空,且阈值也为空(HashTable调用无参数构造函数创建,第一次执行resize)

在这里HashMap最主要的几个函数就已经分析完了,如有需要请查看JDK源码。
分析一下HashTable和HashMap的区别:

  • 1、HashTable是线程安全的而HashMap不是线程安全的,这一点显而易见
  • 2、HashTable的key和value不能为空,而HashMap的key和value可以为空。通过hash运算可以看出,HashMap中key为null的Node,hash值为0,则对于的节点都是存储在槽位为0(即第一个槽位上)
  • 3、HashTable中的节点以链表形式存储,而HashMap中,jdk1.8及以后既有链表也有红黑树
  • 4、HashTable的table长度不是2的幂次,而HashMap的table长度是2的幂次,所有HashMap的效率会更高。下标运算时使用或运算比取余运算更加的高效.
  • 5、HashMap在求key的hashcode时,引入了高16位,减少了hash冲突

ConcurrentHashMap

虽然HashMap从很多地方看,在HashTable的基础上做了很大的修改和提升,但依旧有很大的问题。如果是单线程的情况下,使用HashMap的效率很高,但是在多线程并发的情况下,HashMap就会出现问题。有一种解决方案就是给HashMap对象加锁,但是这样虽然防止了对象共享的问题,但整个对象加锁的效率就会非常低,即使是多读少写的情况,无论是读或者写,当一个线程拿到当前HashMap的锁后,其他的线程都不能进行操作只能等待当前线程操作结束释放锁。在这个基础上,ConcurrentHashMap的出现就给并发情况下的HashMap提供了解决方案。

注意:因为jdk 1.8之前的ConcurrentHashMap使用的Segment分段锁技术,而1.8后就舍弃了分段锁技术,采用了CAS乐观锁机制,只有在节点头访问时例如table[i]才会加锁,对其中的变量采用cas院子操作


继承和实现
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable 

我们可以看到,此处ConcurrentHashMap 继承了AbstractMap,和HashMap相同。但是这里ConcurrentHashMap实现了ConcurrentMap,与HashMap不同。我们通过ConcurrentMap可以看到

public interface ConcurrentMap<K, V> extends Map<K, V>

ConcurrentMap继承了接口Map,由此可见,此处和HashMap中实现Map接口是一样的。不过,在代码上将,ConcurrentMap的方法和AbstractMap的方式虽然都来自Map,但继承和实现却不同。
此处,我们可能会产生一个疑惑,为什么ConcurrentMap接口继承了Map接口,在Thinking in java中也没有讲到接口继承的问题,但我们都知道Java中,接口可以多实现,只能单继承。而接口不能实现另一个接口,所以一个接口可以继承其他的接口。


常量和成员变量

常量
下面说几个在JDK1.8中新增的常量

/*
 * 最大的扩容线程的数量,如果上面的 RESIZE_STAMP_BITS = 32,那么此值为 0,这一点也很奇怪。
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/*
 * 移位量,把生成戳移位后保存在sizeCtl中当做扩容线程计数的基数,相反方向移位后能够反解出生成戳 
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

/* ForwardingNode的hash值,ForwardingNode是一种临时节点,在扩进行中才会出现,并且它不存储实际的数据
 * 如果旧数组的一个hash桶中全部的节点都迁移到新数组中,旧数组就在这个hash桶中放置一个ForwardingNode
 * 读操作或者迭代读时碰到ForwardingNode时,将操作转发到扩容后的新的table数组上去执行,写操作碰见它  * 时,则尝试帮助扩容 */
static final int MOVED     = -1; // hash for forwarding nodes

/* TreeBin的hash值,TreeBin是ConcurrentHashMap中用于代理操作TreeNode的特殊节点,持有存储实际数据的红黑树的根节
*  点因为红黑树进行写入操作,整个树的结构可能会有很大的变化,这个对读线程有很大的影响,所以TreeBin    还要维护一个简单读写锁,这是相对HashMap,这个类新引入这种特殊节点的重要原因
*/
static final int TREEBIN   = -2; // hash for roots of trees

/* ReservationNode的hash值,ReservationNode是一个保留节点,就是个占位符,不会保存实际的数据,正常情况是不会出现的,在jdk1.8新的函数式有关的两个方法computeIfAbsent和compute中才会出现*/
static final int RESERVED  = -3; // hash for transient reservations

/* 用于和负数hash值进行 &运算,将其转化为正数(绝对值不相等),Hashtable中定位hash桶也有使用这种方式来进行负数转正数*/
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash


成员变量

    //当前的table数组
    transient volatile Node<K,V>[] table;

    //扩容后的新的table数组,只有在扩容时不为空
    //通过判断newTable !=null,一般可以认为当前还有线程在进行扩容
    private transient volatile Node<K,V>[] nextTable;

    /**
     *下面三个主要与统计数目有关,
     *可以参考jdk1.8新引入的java.util.concurrent.atomic.LongAdder的源码,帮助理解
     *计数器基本值,主要在没有碰到多线程竞争时使用,需要通过CAS进行更新
     */
    private transient volatile long baseCount;

    /**
     * 非常重要的一个属性,具体的含义通过后面扩容进行解读。
     * 其实也就是一个扩容的阈值以及用于多线程的扩容互斥(即当一个线程扩容时,其他线程不能扩容)
     */
    private transient volatile int sizeCtl;

    /**
     * 保证一个transfer任务不会被几个线程同时获取(相当于任务队列的size减1)
     */
    private transient volatile int transferIndex;

    /**
     *CAS自旋锁标志位,用于初始化,或者counterCells扩容时
     */
    private transient volatile int cellsBusy;

    /**
     * 用于高并发的计数单元,如果初始化了这些计数单元,那么跟table数组一样,长度必须是2^n的形式
     */
    private transient volatile CounterCell[] counterCells;

可以看到所有的变量都是通过volatile关键字进行了修饰,对于volatile关键的原理请查询相关资料,这里简单说一下,通过volatile关键字修饰的变量,保证了内存可见性以及禁止指令重排,不能保证原子性(即volatile单独使用不能解决并发问题)


在ConcurrentHashMap中,Node也是继承了Map.Entry

static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock

        static int tieBreakOrder(Object a, Object b) {
            int d;
            if (a == null || b == null ||
                (d = a.getClass().getName().
                 compareTo(b.getClass().getName())) == 0)
                d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                     -1 : 1);
            return d;
        }

        /**
         * Creates bin with initial set of nodes headed by b.
         */
        TreeBin(TreeNode<K,V> b) {
            super(TREEBIN, null, null, null);
            this.first = b;
            TreeNode<K,V> r = null;
            for (TreeNode<K,V> x = b, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (r == null) {
                    x.parent = null;
                    x.red = false;
                    r = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = r;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);
                            TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            r = balanceInsertion(r, x);
                            break;
                        }
                    }
                }
            }
            this.root = r;
            assert checkInvariants(root);
        }
 //...................省略其他的方法

在ConcurrentHashMap中,还有一个辅助内部类,ForwardingNode。他继承于Node节点,是一个临时节点,在扩容进行中才会出现,hash值固定为-1,并且它不存储实际的数据。在扩容的时候,旧数组的一个槽位中的节点全部迁移到新数组中,旧数组就在这个槽位上放置一个ForwardingNode。当读操作或者迭代操作碰到ForwardingNode时,将作转发到扩容后的新的table数组上去执行,写操作碰到时,将会直接在新的tab上写。

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null); // 此节点hash=-1,key、value、next均为null
        this.nextTable = tab;
    }
    Node<K,V> find(int h, Object k) {
        // 查nextTable节点,outer避免深度递归
        outer: for (Node<K,V>[] tab = nextTable;;) {
            Node<K,V> e; intn;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                returnnull;
            for (;;) { // CAS算法多和死循环搭配!直到查到或null
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    returne;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        continue outer;
                    }
                    else
                        return e.find(h, k);
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

注意:在forwarding中,值是一个标志节点,指向newTable,并提供一个find方法,帮助找到在新的table中index对应的节点。


方法函数

构造函数

 public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

构造函数主要做了两件事(和HashMap差不多):

  • 1、参数的有效性检查
  • 2、table初始化的长度(如果不指定默认情况下为16)。
    这里要说一个参数:concurrencyLevel,表示能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认值为16,(即允许16个线程并发可能不会产生竞争)。仅仅是为了兼容旧版本而保留

三个原子操作

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // 获取索引i处Node
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
    // 利用CAS算法设置i位置上的Node节点(将c和table[i]比较,相同则插入v)。
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
    // 设置节点位置的值,仅在上锁区被调用
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

这三个原子操作调用频率很高。而且原子操作的效率和并发性都得到了保证

先讲最简单的get方法
get方法很简单,对于key的hash和value查到相应的值,节点可能对于链表或者红黑树

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;
    }

在这里有一个spread二次hash运算,和HashMap中的hash函数相同,也是将高16位引入计算并且保证为正数

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
static final int HASH_BITS = 0x7fffffff;    

put方法

  public V put(K key, V value) {
        return putVal(key, value, false);
    }

在put方法中调用putVal,下面我们分析putVal的源码


    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //判断key和value是否为空,如果为空就抛出异常
        if (key == null || value == null) throw new NullPointerException();
        //通过二次hash运算得到key的hashcode
        int hash = spread(key.hashCode());
        //用于记录相应链表的长度
        int binCount = 0;
        //开始死循环,直到插入成功
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果数组为空,则进行初始化。对应的函数在后面会分析
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //否则,通过原子操作tabAt找到index对应的头节点
            //如果为空就通过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
            }
            //如果对应的位置不为空,判断是否等于MOVED,如果等于MOVED,表示当前槽位正在进行扩容
            else if ((fh = f.hash) == MOVED)
             //帮助数据迁移,等后面分析对应的源码就很容易理解
                tab = helpTransfer(tab, f);
            //如果没有进行扩容
            else {
                V oldVal = null;
                //对数组该位置的头结点进行加锁
                synchronized (f) {
                 //判断在查找的过程中是否有改变,通过再次获取头结点以及判断是否相等
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {  如果hash值大于0表示是链表
                            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)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //否则将Node假如到链表末尾
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果是树节点,将Node添加到红黑树中
                        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方法看完,留下三个问题,第一个是初始化table,第二个是扩容,第三个问题是帮助数据迁移(helpTrasfer)。下面将会进行一一介绍

初始化:initTable

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //判断当前的table是否为空,如果为空则进行初始化
        while ((tab = table) == null || tab.length == 0) {
            //这里使用sizeCtl判断是否其他线程正在进行初始化
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
                //如果没有其他线程正在进行初始化,则通过CAS将sizeCtl设置为-1,表示抢到了锁
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {

                    if ((tab = table) == null || tab.length == 0) {
                        //DEFAULT_CAPACITY 初始容量为16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //这里就是ConcurrentHashMap为什么不适用LoadFactory的原因了
                        //如果n=16,则sc = 12,相当于0.75*n
                        sc = n - (n >>> 2);
                    }
                } finally {
                   //最后将sizeCtl设置为sc大小,扩容结束,并且
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

treeifyBin
这个函数刚会在put方法中也见到过,就是将链表转换为红黑树,但是treeifyBin不一定会进行转换,也可能仅仅是对数组进行扩容。下面我们将会具体讲到相关的源码。

 private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
              //首先判断数组长度是否小于64,如果小于64(其实就是16,32等)就会进行数组扩容
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                //扩容函数,后面讲到
                tryPresize(n << 1);
                //不扩容,找到对应index的头结点
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) { //加锁,进行转换
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        //下面就是遍历链表,转换为红黑树
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //最后通过原子操作将红黑树设置到对应的数组位置上
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

在这里大家可能会有一个疑问:为什么数组长度小于64,就只进行扩容而不转换为红黑树呢。首先,MIN_TREEIFY_CAPACITY是一个系统定义的值,当数组长度小于64时,对数组进行扩容,能够明显的解决hash冲突从而减小链表长度以此来避免对链表转化为红黑树


扩容

 private final void tryPresize(int size) {

        //如果size大于等于默认最长数组长度的1/2,则使用默认最长的长度。否则将size*1.5+1,向上取最近的2的n次方(其实这里也有一个疑问,在整个ConcurrentHashMap中,table的长度都是2的幂次,为什么这里不直接使用size。而且在调用扩容时,先讲table.len<<1,这里再将size*1.5去最近的2的幂次不是相当于4倍了么)
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //执行扩容,后面的和初始化相差不大,最主要的就是调用transfer进行迁移
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //调用transfer进行迁移,后面会讲到
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

transfer
下面这个方法很点长,将原来的 tab 数组的元素迁移到新的 nextTab 数组中。

虽然我们之前说的 tryPresize 方法中多次调用 transfer 不涉及多线程,但是这个 transfer 方法可以在其他地方被调用,典型地,我们之前在说 put 方法的时候就说过了,请往上看 put 方法,是不是有个地方调用了 helpTransfer 方法,helpTransfer 方法会调用 transfer 方法的。

此方法支持多线程执行,外围调用此方法的时候,会保证第一个发起数据迁移的线程,nextTab 参数为 null,之后再调用此方法的时候,nextTab 不会为 null。

阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 这里使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。

第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;

    // stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
    // stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
    //   将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    // 如果 nextTab 为 null,先进行一次初始化
    //    前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
    //       之后参与迁移的线程调用此方法时,nextTab 不会为 null
    if (nextTab == null) {
        try {
            // 容量翻倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // nextTable 是 ConcurrentHashMap 中的属性
        nextTable = nextTab;
        // transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
        transferIndex = n;
    }

    int nextn = nextTab.length;

    // ForwardingNode 翻译过来就是正在被迁移的 Node
    // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
    // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
    //    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
    //    所以它其实相当于是一个标志。
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);


    // advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab

    /*
     * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
     * 
     */

    // i 是位置索引,bound 是边界,注意是从后往前
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 下面这个 while 真的是不好理解
        // advance 为 true 表示可以进行下一个位置的迁移了
        //   简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;

            // 将 transferIndex 值赋给 nextIndex
            // 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                // 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                // 所有的迁移操作已经完成
                nextTable = null;
                // 将新的 nextTab 赋值给 table 属性,完成迁移
                table = nextTab;
                // 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }

            // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
            // 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
            // 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 任务结束,方法退出
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;

                // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 头结点的 hash 大于 0,说明是链表的 Node 节点
                    if (fh >= 0) {
                        // 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
                        // 需要将链表一分为二,
                        //   找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
                        //   lastRun 之前的节点需要进行克隆,然后分到两个链表中
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 其中的一个链表放在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 另一个链表放在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        // 红黑树的迁移
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;

                        // 将 ln 放置在新数组的位置 i
                        setTabAt(nextTab, i, ln);
                        // 将 hn 放置在新数组的位置 i+n
                        setTabAt(nextTab, i + n, hn);
                        // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                        //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                        setTabAt(tab, i, fwd);
                        // advance 设置为 true,代表该位置已经迁移完毕
                        advance = true;
                    }
                }
            }
        }
    }
}

helptransfer

   /**
     * Helps transfer if a resize is in progress.
     */
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

helptransfer的原理很简单,就是判断当前节点是否已经搬移到新的table中,如果是,则表示当前table正在进行扩容,这个线程将会帮助扩容。如果没有搬迁,则在当前节点上进行操作


不得不说在ConcurrentHashMap中,需要理解的方法很多,这也是在高效提升后所带来的代码复杂性。尤其是在JDK 1.8后,对ConcurrentHashMap的改进后,理解起来更加的困难。但我们从源码上一句一句的理解过后,我们会发现很多东西。下面将会对ConcurrentHashMap做一个总结,包括与HashMap的比较以及在JDK 1.8之前ConcurrentHashMap与JDK 1.8有什么区别。

对于HashMap和ConcurrentHashMap的区别

  • HashMap不是线程安全的,ConcurrentHashMap是线程安全的。
  • HashMap的key和value可以为null,ConcurrentHashMap不能为空
  • 当线程安全的使用HashMap时,ConcurrentHashMap效率会高出很多

对于JDK1.7和JDK1.8中ConcurrentHashMap的区别

  • 最重要的是安全机制,jdk1.7中 采用的是Segment分段锁机制,Segment继承于ReentrantLock这种可重入锁。对每一段table进行加锁。而在Jdk1.8中,舍弃了分段锁机制,采用CAS+Synchronized机制实现了更小粒度的锁,即对table的每一个index进行加锁同样采用很多的原子操作,也更加的提升了效率。
  • 在put方法,1.7通过两次hash运算,先后得到哪一段,哪一个index,获取到对应的链表或者红黑树,最后通过trylock()对整段进行加锁,其他线程挂起。1.8中,通过hash运算得到index,获取到头结点,此时有三种情况。如果头结点为空,则直接通过CAS操作将Node put进去。如果不为空但hash为move,则表示有其他线程正在进行扩容,参与一起扩容,头结点不为空且hash不为move,则对头结点进行synchronized加锁,遍历put。
  • get 方法大致相同,因为变量都是通过volatile修饰,保证了内存可见性。当修改数据后,能够将缓存中的值立马写到主存中,保证并发下能够读到最新的数据。
  • resize方法 1.7是对每个segment进行扩容,和HashMap相似,不过会进行lock,是单线程进行扩容。而在1.8中,则能够进行多线程扩容,让访问到对应的节点为Forwarding,则参与扩容。
  • size()方法,在1.7中采用先不加锁获取两次,如果相同则正确,不一样则把所有的segment锁住,计算size。在1.8中,采用了一个baseCount变量来记录当前节点的个数,通过调用sumCount来计算baseCount和counterCells存储的修改次数的和,最后获取到size。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值