ConcurrentHashMap

1、ConcurrentHashMap

官方文档介绍:

键和值是不允许null存在的。
一个支持并发操作的哈希表,是线程安全的。操作方式与Hashtable一致。
获取数据的操作是非阻塞的,所以在并发读写的过程中,读取到的数据可能已经过时。
迭代器被设计为一次只能由一个线程使用.
聚合了状态的方法(如size,isEmpty,containsValue等)是返回Map瞬时状态的信息,可用于做做监控但是不能用作程序控制。
由于Map或进行扩容处理,扩容过程可能相对较慢,所以最好提供一个合理的初始值进行初始化。

2、类层次结构

  • Map-AbstractMap这条线给出了Map操作的骨架实现,
  • Map-ConcurrentMap这条线定义了支持同步的Map基本骨架。

3、构造函数


ConcurrentHashMap提供了5个构造函数,可以根据参数初始化ConcurrentHashMap

3.1、public ConcurrentHashMap()

创建一个使用默认值的空Map。Map容器的初始化延迟到具体操作。

public ConcurrentHashMap() {
}

3.2、public ConcurrentHashMap(int initialCapacity)

创建一个空Map,根据参数设置Map容器的初始化大小,Map容器的初始化延迟到具体操作。

/**存储数据的容器,创建Map对象是不会初始化容器大小,初始化延迟到第一次数据写入,容器的大小总是2的幂(与后面计算元素的位置有关),可以通过迭代器直接访问容器*/
transient volatile Node<K,V>[] table;
/**
控制容器初始化和扩容的状态,默认值为0,负数和整数表示两种状态,
负数时:
      -1表示容器正在初始化或者扩容,
      -n(小于-1)表示n-1个线程正在扩容操作,
正数时:
      如果容器为null,则表示容器初始化的长度,
      如果容器不为null,则表示下一次发生扩容的容器长度
*/
private transient volatile int sizeCtl;
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               // 取大于 1.5*initialCapacity+1 的最小2的幂
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

官方文档对sizeCtl的描述不太准确,具体看这篇文章
由于table容器的长度必须是2的幂,所以tableSizeFor会根据参数获取大于 1.5*initialCapacity+1 的最小2的幂

3.2.1、int tableSizeFor(int c)

/**
根据参数c容量,返回一个大于c的最小2的幂
文档指出了此算法的出处 《Hackers Delight》,第3.2节
*/
private static final int tableSizeFor(int c) {
    int n = c - 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;
}

这里使用了5个连续的位移和或运算,得到一个二进制全为1的数,这个数+1后就得到一个2的幂.
1、假设c=20,二进制为 0000 0000 0000 0000 0000 0000 0010 0100
2、向右无符号位移一位,得到 0000 0000 0000 0000 0000 0000 0001 0010
3、进行或运算
0000 0000 0000 0000 0000 0000 0010 0100
0000 0000 0000 0000 0000 0000 0001 0010
————————————————————————————————————————————————
0000 0000 0000 0000 0000 0000 0011 0110
4、对 0000 0000 0000 0000 0000 0000 0011 0110 无符号右移两位 得到 0000 0000 0000 0000 0000 0000 0000 1101
5、或运算
0000 0000 0000 0000 0000 0000 0011 0110
0000 0000 0000 0000 0000 0000 0000 1101
————————————————————————————————————————————————
0000 0000 0000 0000 0000 0000 0011 1111
6、观察后可以推算出,每次无符号右移n位,就是将最大位为1的位置,向右连续取n位为1
java中int占4个字节,1个字节8个bit,所以int用32bit表示,1+2+4+8+16=31,由于最高位表示符号位,所以5个连续的位移或运算得到一个从最大位开始到低位全为1的值,也就是2的幂-1

3.2.2、与HashMap构造函数对比

参数 初始容器值算法 含义
HashMap initialCapacity 取大于 initialCapacity 的最小2的幂 initialCapacity 就是实际想要初始化容器长度的值,但是由于容器长度必须是2的幂,所以计算最接近initialCapacity的2的幂,然后设置为扩容阈值,但由于初始化的特殊性,初始化容器时,会将这个2的幂作为容器长度初始化容器。例如如果初始化设置initialCapacity 为15,则计算2的幂得到16,然后第一次put时,table的长度会被设置为16,扩容阈值threshold被重置为16*0.75,所以,再编写代码时,如果我们查询到一个List,要将数据分组放到Map中以便后面方便取,则可以使用 1.5 * list.size 去初始化Map以减少扩容的发生
ConcurrentHashMap initialCapacity 取大于 1.5*initialCapacity+1 的最小2的幂 initialCapacity更像是想要插入map中的数据长度,然后ConcurrentHashMap根据这个长度计算出一个插入initialCapacity个元素也不会产生扩容的值。例如如果初始化设置initialCapacity为15,则通过计算得到2的幂为32,第一次put时,table的长度会被设置为32,扩容阈值sizeCtl被重置为32*0.75,所以,再编写代码时,如果我们查询到一个List,要将数据分组放到Map中以便后面方便取,则可以使用 list.size 去初始化Map以减少扩容的发生

3.3、public ConcurrentHashMap(Map<? extends K, ? extends V> m)

/**
根据指定Map创建一个新Map
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 设置table容器默认初始值16
    this.sizeCtl = DEFAULT_CAPACITY;
    // 插入数据
    putAll(m);
}
/**
1、为了防止参数Map的数据较多导致多次扩容,因为CurrentHashMap的扩容耗时较长,所以首先要先根据参数Map的数据个数先尝试对容器进行扩容,达到减少发生扩容次数的目的
为什么说是尝试,因为有可能现有table容器长度已经满足数据写入要求,无需扩容
2、循环参数Map,插入数据到容器中
*/
public void putAll(Map<? extends K, ? extends V> m) {
    // 以指定Map的size尝试进行扩容
    tryPresize(m.size());
    // 循环插入数据
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}

3.3.1、private final void tryPresize(int size)

/**
根据大小尝试对容器进行扩容
流程大致如下,由于这里是构造函数进来,所以先只看初始化容器的代码
判断table容器是否为空
      是
            使用CAS算法修改sizeCtl 
                  修改成功
                        判断table容器是否发生变更
                              否
                                    创建初始化table容器,计算下一次扩容阈值,设置sizeCtl 
                                    
*/
private final void tryPresize(int size) {
    // 获取大于1.5*size+1的最小2的幂
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    /**
      上面提到sizeCtl是有可能小于0的,只有当sizeCtl >= 0 时才能进行扩容,从ConcurrentHashMap(Map<? extends K, ? extends V> m)构造函数进来时,此值为16
      这里使用了while,说明仅当sc < 0 时才会跳出循环,上面提到sizeCtl=-1表示正在扩容或者初始化,所以循环内必定会修改sc的值
    */ 
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        if (tab == null || (n = tab.length) == 0) { // 如果table容器为空,则进行容器初始化
            n = (sc > c) ? sc : c; // 比较获取初始化容器的长度,sizeCtl在构造函数进来时会初始化为默认值16,c是根据参数Map计算出来满足初始要求的最小容器长度,两者比较取其大
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 使用CAS算法更新sizeCtl的值为-1,SIZECTL为sizeCtl的偏移量,在ConcurrentHashMap类加载时就会初始化,sc为期望值,-1位更新值,
                try {
                    if (table == tab) { // 判断是否有其他线程修改了容器,由于并发场景下,上面CAS锁成功了,存在其他线程已经完成容器初始化甚至扩容的动作,所以要做判断
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 创建容器
                        table = nt; // 初始化容器
                        sc = n - (n >>> 2); // n >>> 2 就是 n/4, n - n/4 = 0.75n,实际就是加载因子*容器长度
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY) // 跳出循环,当前容器已经无需扩容或者已达到容器最大边界
            break;
        else if (tab == table) {// 判断当前容器是否被其他线程初始化
            int rs = resizeStamp(n); // resizeStamp作用不是很明白,看名字与扩容相关, TODO 存在疑问,记录下,待了解
            if (sc < 0) { // 
                Node<K,V>[] nt;
                // 由于rs看不懂,所以这个if也不清楚是要干啥,但是break,意味着进入这个if就要跳出循环了,那么这个if推算应该是判断有其他线程正在进行扩容之类的操作,因为我要干的事已经有人在帮我干了,所以我直接退出,猜测的,不知道对不对
                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)) // 还是没看懂,感觉智商不够用,CAS算法将sizeCtl的值+1,
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

完成table容器初始化后,就是循环参数Map,将数据一条一条插入到table容器中,putVal后面再单独说
大致过程总结下就是:根据参数map的size计算一个扩容值,比较扩容值和默认值,取其大,然后初始化table容器并设置

3.4、ConcurrentHashMap(int initialCapacity, float loadFactor)

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

调用其他构造函数,并设置了个默认值1

3.5、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

根据指定的初始容器长度和加载因子和并发数创建Map容器,这里要注意与HashMap不同的是ConcurrentHashMap是不允许修改loadFactor的,这里参数loadFactor只是用来计算初始容器长度值使用

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

不太清楚提供加载因子的构造函数参数的意义在哪里,既然不提供加载因子的修改,那么算出来的size对于ConcurrentHashMap来说是不准确的。
例如,initialCapacity=12,loadFactor=0.85,则size=15,cap=16,而ConcurrentHashMap的加载因子是0.75,16*0.75=12,在插入到第12条数据的时候就会发生扩容

4、常用方法

4.1、public V put(K key, V value)

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

put(K key, V value)方法是对外提供插入数据的接口
实际插入的逻辑在putVal(key, value, false)中

4.2、final V putVal(K key, V value, boolean onlyIfAbsent)

key是插入的键,value是插入的值,onlyIfAbsent表示是否在key不存在时插入

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); // 与HashMap类似,通过低16位与高16位异或操作,区别在于ConcurrentHashMap多了跟Integer.MAX_VALUE进行与运算操作
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0) // 如果table容器为空,则进行容器初始化
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // (n - 1) & hash计算得到容器下标并赋值给i,f为下标对应的节点信息
            // CAS算法在下标i设置节点信息,如果设置成功,则跳出循环,如果不成功,重新进入循环执行逻辑
            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) // 判断容器是否正在扩容,这里可以看出,扩容时,会修改链表头节点的hash值
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 进入这里,说明容器已完成初始化,且没有发生扩容,则需要将节点信息更新到链表或者树中,synchronized同步锁锁住了链表头结点,说明最高支持table.length个线程并行写
            synchronized (f) { 
                if (tabAt(tab, i) == f) { // 判断下标位的链表表头是否发生改变(例如其他线程进行了remove操作)
                    /**
                        查看后面的else if,可以推断出,树节点的hash值一定<0,再观察if中的代码,是对链表的遍历,所以可以猜测,链表的节点hash值为正,红黑树节点的hash值为负,但是因为
                        后面是else if而不是else,所以这里是否说明 红黑树的节点hash值一定为负数,但是节点hash值为负数不一定就是红黑树节点?
                    */
                    if (fh >= 0) { 
                        binCount = 1;
                        // 循环遍历链表,binCount为链表长度,e为当前链表节点信息
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果key已经存在,则根据onlyIfAbsent判断是否更新数据,然后跳出循环,结束插入操作
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 节点后移,判断节点是否为空,为空说明循环到了链表尾部,key不存在,需要在尾部插入新节点
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 如果下标位的节点hash值小于0,判断是否是树节点
                        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;
                        }
                    }
                }
            }
            // 判断链表是否需要树形化,如果上面是在树中插入数据,bitCount为2,不会执行 if(binCount >= TREEIFY_THRESHOLD)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    /*
      添加统计数,方法还蛮复杂,处理统计,还会做扩容时的数据转移等操作
    */
    addCount(1L, binCount);
    return null;
}
总结:
      1、key、value校验
      2、获取key的hash值
      3、死循环,直到数据插入成功退出
            判断容器是否为空
                  是:初始化容器,初始化完成后继续3,重新执行逻辑
                  否:根据hash值获取容器下标,判断容器下标节点是否为空
                        是:使用CAS更新下标位数据
                              更新成功:跳出3循环,执行4
                              更新失败:说明存在并发写,重新执行3,为了将数据写入到链表末尾
                        否:判断hash值是否为-1(扩容)
                              是:容器正在扩容,帮助节点数据转移
                              否:  synchronized同步锁锁住下标节点信息,进行数据写入
                                    判断容器下标节点是否发生改变,防止数据被删除
                                    判断数据是否写入成功      
                                          是:根据链表长度判断是否需要链表树化
                                                是:树化链表
                                              跳出3执行4  
                                          否:重新执行3
      4、添加统计数,如果发生扩容,则辅助数据转移

4.3、public V get(Object key)

根据key获取value

public V get(Object 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值和key相等的节点信息,如果没有匹配到,返回null
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历链表节点,匹配hash值和key相等的节点信息
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
总结:
      1、获取key的hash值
      2、判断容器不为空且容器下标节点不为空
            是
                  下标节点hash与1的hash值是否相等
                        是
                              判断下标节点的key是否与参数key相等
                                    是
                                          返回下标节点值
                        否
                              下标节点的hash值是否小于0
                                    是
                                          遍历节点信息(树),返回hash值和key值相等的节点信息,匹配失败则返回null
                  遍历节点信息(链表),返回hash值和key值相等的节点信息
      3、返回null

4.4、public V remove(Object key)

根据key删除节点

// 对外删除入口
public V remove(Object key) {
    return replaceNode(key, null, null);
}
// 实际删除逻辑代码
final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        validated = true;
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

TODO 未完

posted @ 2021-01-11 17:41  Simple°  阅读(161)  评论(0编辑  收藏  举报