源码解析之——ConcurrentHashMap

底层实现:数组+链表+红黑树
链表长度为8时转换为红黑树,为6时转换为链表
数组中75%的位置被使用时进行扩容。

ConcurrentHashMap 此外提供了线程安全的保证,它主要是通过 CAS 和 Synchronized 关键字来实现。

源码参数

1、transient volatile Node<K,V>[] table

这个 Node 数组就是 ConcurrentHashMap 用来存储数据的哈希表。

2、private static final int DEFAULT_CAPACITY = 16;

这是默认的初始化哈希表数组大小

3、static final int TREEIFY_THRESHOLD = 8

转化为红黑树的链表长度阈值

4、static final int MOVED = -1

这个标识位用于识别扩容时正在转移数据

5、static final int HASH_BITS = 0x7fffffff;

计算哈希值时用到的参数,用来去除符号位

6、private transient volatile Node<K,V>[] nextTable;

数据转移时,新的哈希表数组

组成元素:

Node

链表中的元素为 Node 对象。他是链表上的一个节点,内部存储了 key、value 值,以及他的下一个节点的引用。这样一系列的 Node 就串成一串,组成一个链表。

ForwardingNode

当进行扩容时,要把链表迁移到新的哈希表,在做这个操作时,会在把数组中的头节点替换为 ForwardingNode 对象。ForwardingNode 中不保存 key 和 value,只保存了扩容后哈希表(nextTable)的引用。此时查找相应 node 时,需要去 nextTable 中查找。

TreeBin

当链表转为红黑树后,数组中保存的引用为 TreeBin,TreeBin 内部不保存 key/value,他保存了 TreeNode 的 list 以及红黑树 root。

TreeNode

红黑树的节点。

源码分析

1、put方法

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

实际调用的是 putVal 方法,第三个参数传入 false,控制 key 存在时覆盖原来的值。

final V putVal(K key, V value, boolean onlyIfAbsent) {
  //key和value不能为空
    if (key == null || value == null) throw new NullPointerException();
  //计算key的hash值,后面我们会看spread方法的实现
    int hash = spread(key.hashCode());
    int binCount = 0;
  //开始自旋,table属性采取懒加载,第一次put的时候进行初始化
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
      //如果table未被初始化,则初始化table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
      //通过key的hash值映射table位置,如果该位置的值为空,那么生成新的node来存储该key、value,放入此位置
        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
        }
      //如果该位置节点元素的hash值为MOVED,也就是-1,代表正在做扩容的复制。那么该线程参与复制工作。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
      //下面分支处理table映射的位置已经存在node的情况
        else {
            V oldVal = null;
            synchronized (f) {
              	//再次确认该位置的值是否已经发生了变化
                if (tabAt(tab, i) == f) {
                  //fh大于0,表示该位置存储的还是链表
                    if (fh >= 0) {
                        binCount = 1;
                      //遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                          //如果存在一样hash值的node,那么根据onlyIfAbsent的值选择覆盖value或者不覆盖
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                          //如果找到最后一个元素,也没有找到相同hash的node,那么生成新的node存储key/value,作为尾节点放入链表。
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                  //下面的逻辑处理链表已经转为红黑树时的key/value保存
                    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保存完成后,判断链表长度是否已经超出阈值,则进行哈希表扩容或者将链表转化为红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  	//计数,并且判断哈希表中使用的桶位是否超出阈值,超出的话进行扩容
    addCount(1L, binCount);
    return null;
}

在这里插入图片描述

// 传入的参数 h 为 key 对象的 hashCode,spreed 方法对 hashCode 进行了加工。重新计算出 hash。
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

?为什么不直接用key的hashcode

为了减少碰撞的概率。
1、h ^ (h >>> 16)
h >>> 16 的意思是把 h 的二进制数值向右移动 16 位。我们知道整形为 32 位,那么右移 16 位后,就是把高 16 位移到了低 16 位。而高 16 位清 0 了。
^ 为异或操作,二进制按位比较,如果相同则为 0,不同则为 1。这行代码的意思就是把高低 16 位做异或。如果两个 hashCode 值的低 16 位相同,但是高位不同,经过如此计算,低 16 位会变得不一样了。为什么要把低位变得不一样呢?这是由于哈希表数组长度 n 会是偏小的数值,那么进行 (n - 1) & hash 运算时,一直使用的是 hash 较低位的值。那么即使 hash 值不同,但如果低位相当,也会发生碰撞。而进行 h ^ (h >>> 16) 加工后的 hash 值,让 hashCode 高位的值也参与了哈希运算,因此减少了碰撞的概率。
2、(h ^ (h >>> 16)) & HASH_BITS
我们再看完整的代码,为何高位移到低位和原来低位做异或操作后,还需要和 HASH_BITS 这个常量做 & 计算呢?HASH_BITS 这个常量的值为 0x7fffffff,转化为二进制为 0111 1111 1111 1111 1111 1111 1111 1111。这个操作后会把最高位转为 0,其实就是消除了符号位,得到的都是正数。这是因为负的 hashCode 在 ConcurrentHashMap 中有特殊的含义,因此我们需要得到一个正的 hashCode。

initTable 源码分析

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
      //如果sizeCtl<0,那么有其他线程正在创建table,所以本线程让出CPU的执行权。直到table创建完成,while循环跳出。if中同时还把sizeCtl的值赋值给了sc。
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
      //以CAS方式修改sizeCtl为-1,表示本线程已经开始创建table的工作。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
              //再次确认是否table还是空的
                if ((tab = table) == null || tab.length == 0) {
                  //如果sc有值,那么使用sc的值作为table的size,否则使用默认值16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                  //sc被设置为table大小的3/4
                    sc = n - (n >>> 2);
                }
            } finally {
              	//sizeCtl被设置为table大小的3/4
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

关键的值 sizeCtl,这个值有多个含义。
1、-1 代表有线程正在创建 table;
2、-N 代表有 N-1 个线程正在复制 table;
3、在 table 被初始化前,代表根据构造函数传入的值计算出的应被初始化的大小;
4、在 table 被初始化后,则被设置为 table 大小 的 75%,代表 table 的容量(数组容量)。

Put 方法中,保存 key/value 源码分析

			V oldVal = null;
            synchronized (f) {
              	//再次确认该位置的值是否已经发生了变化
                if (tabAt(tab, i) == f) {
                  //fh大于0,表示该位置存储的还是链表
                    if (fh >= 0) {
                        binCount = 1;
                      //遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                          //如果存在一样hash值的node,那么根据onlyIfAbsent的值选择覆盖value或者不覆盖
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                          //如果找到最后一个元素,也没有找到相同hash的node,那么生成新的node存储key/value,作为尾节点放入链表。
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                  //下面的逻辑处理链表已经转为红黑树时的key/value保存
                    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;
                        }
                    }
                }
            }

这段代码主逻辑如下:
第一种情况:hash 值映射哈希表对应位置存储的是链表:
1、遍历 hash 值映射位置的链表;
2、如果存在同样 hash 值的 node,那么根据要求选择覆盖或者不覆盖;
3、如果不存在同样 hash 值的 node,那么创建新的 node 用来保存 key/value,并且放在链表尾部。
第二种情况:hash 值映射哈希表对应位置存储的是红黑树:
通过 TreeBin 对象的 putTreeVal 方法保存 key/value

以上逻辑还是比较清晰和简单。我们继续往下看,保存完 key/value 后,其实并没有结束 put 操作,而是进行了扩容的操作:

if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal;
    break;
}

binCount 是用来记录链表保存 node 的数量的,可以看到当其大于 TREEIFY_THRESHOLD,也就是 8 的时候进行扩容。
当固定大小的哈希表存储数据越来越多时,链表长度会越来越长,这会造成 put 和 get 的性能下降。此时我们希望哈希表中多一些桶位,预防链表继续堆积的更长。接下来我们分析 treeifyBin 方法代码,这个代码中会选择是把此时保存数据所在的链表转为红黑树,还是对整个哈希表扩容。

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
      //如果哈希表长度小于64,那么选择扩大哈希表的大小,而不是把链表转为红黑树
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
      //将哈希表中index位置的链表转为红黑树
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
              	//下面逻辑将node链表转化为TreeNode链表
                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;
                    }
                  //TreeBin代表红黑树,将TreeBin保存在哈希表的index位置
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}
//size为32
//sizeCtl为原大小16的3/4,也就是12
private final void tryPresize(int size) {
  	//根据tableSizeFor计算出满足要求的哈希表大小,对齐为2的n次方。c被赋值为64,这是扩容的上限,扩容一般都是扩容为原来的2倍,这里c值为了处理一些特殊的情况,确保扩容能够正常退出。
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
  //此时sc和sizeCtl均为12,进入while循环
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
      //这里处理的table还未初始化的逻辑,这是由于putAll操作不调用initTable,而是直接调用tryPresize
        if (tab == null || (n = tab.length) == 0) {
          //putAll第一次调用时,假设putAll进来的map只有一个元素,那么size传入1,计算出c为2.而sc和sizeCtl都为0,因此n=2
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                      //经过计算sc=2
                        sc = n - (n >>> 2);
                    }
                } finally {
                  //sizeCtl设置为2.第二次循环时,因为sc和c相等,都为2,进入下面的else if分支,结束while循环。
                    sizeCtl = sc;
                }
            }
        }
      //扩容已经达到C值,结束扩容
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
      //table已经存在,那么就对已有table进行扩容
        else if (tab == table) {
            int rs = resizeStamp(n);
          //sc小于0,说明别的线程正在扩容,本线程协助扩容
            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;
              //未达上限,参与扩容,更新sizeCtl值。transfer方法负责把当前哈希表数据移入新的哈希表。
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
          //本线程为第一个扩容线程,transfer第二个参数传入null,代表需要新建扩容后的哈希表
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

put 方法中最后有如下一行代码

addCount(1L, binCount);

这行代码其实是对哈希表保存的元素数量进行计数。同时根据当前保存状况,判断是否进行扩容。你可能会问,在添加元素的过程中不是已经执行了扩容的逻辑了吗?没错,不过上面的扩容逻辑是链表过长引起的。而 addCount 方法中会判断哈希表是否超过 75% 的位置已经被使用,从而触发扩容。扩容的逻辑是基本一致的。

2、构造函数

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
  //如果传入的初始化容量值超过最大容量的一半,那么sizeCtl会被设置为最大容量。
  //否则通过tableSizeFor方法就算出一个2的n次方数值作为size
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               // 确保了哈希表的大小永远都是 2 的 n 次方.这里传入的参数不是 initialCapacity,而是 initialCapacity 的 1.5 倍 + 1。这样做是为了保证在默认 75% 的负载因子下,能够足够容纳 initialCapacity 数量的元素。
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

1、构造函数中并不会初始化哈希表;
2、构造函数中仅设置哈希表大小的变量 sizeCtl;
3、initialCapacity 并不是哈希表大小;
4、哈希表大小为 initialCapacity*1.5+1 后,向上取最小的 2 的 n 次方。如果超过最大容量一半,那么就是最大容量。

3、get方法

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判断中做了如下几件事情:
  //1、哈希表是否存在
  //2、哈希表是否保存了数据,同时取得哈希表length
  //3、哈希表中hash值映射位置保存的对象不为null,并取出给e,e为链表头节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
      //如果e的hash值和传入key的hash值相等
        if ((eh = e.hash) == h) {
          //如果e的key和传入的key引用相同,或者key eaquals ek。那么返回e的value。
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
      //如果头节点的hash<0,有两种情况
      //1、hash=-1,正在扩容,该节点为ForwardingNode,通过find方法在nextTable中查找
      //2、hash=-2,该节点为TreeBin,链表已经转为了红黑树。同样通过TreeBin的find方法查找。
        else if (eh < 0)
            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;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值