java集合-ConcurrentHashMap源码概述(基于JDK1.8)

    技术2022-07-16  71

    目录

    一、概述

    二、源码说明

    1,构造函数

    1.1 无参的构造函数

    1.2 指定容量、加载因子、并发级别的构造函数

    2,put方法

    2.1 initTable() 初始化Node数组的方法

    2.2 addCount(long x, int check) 对node的数据进行统计,如果需要扩容则扩容

    3,get方法

    4,size方法

    5,remove方法

    三、对比

    1,并发级别

    2,数据结构

    3,链表的插入

    4,扩容


    一、概述

    jdk1.8的ConcurrentHashMap,取消了分段锁的概念,虽然在构造函数中可以指定并发级别这个参数,但是实际只是判断了如果初始容量小于并发级别,则把初识容量的值设置为并发级别的值,然后就没有用到并发级别这个参数了。因为在1.8中,锁更加的细化了,使用对Node数组的每个下标下的头节点元素进行加锁。就相当于容量有多少就有多少锁,锁住的是每一个桶。同时在进行扩容的时候还支持部分数据的访问不阻塞。

    二、源码说明

    1,构造函数

    1.1 无参的构造函数

    public ConcurrentHashMap() { }

    如果是使用默认构造函数的话,初始容量默认为16.

    1.2 指定容量、加载因子、并发级别的构造函数

    出了这个三个参数的构造函数之外,还有另外两个构造函数,分别是:

    ConcurrentHashMap(int initialCapacity):指定初始容量;

    ConcurrentHashMap(int initialCapacity, float 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 // 这里会根据传入的指定容量和加载因子来计算出了size long size = (long)(1.0 + (long)initialCapacity / loadFactor); // 然后会找到一个大于等于size的2的幂次方数 int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); // 对容量进行赋值 this.sizeCtl = cap; }

    2,put方法

    put方法中调用putVal方法

    key和value都不可以为null,否则会抛出异常

    put流程:

    根据key计算出hash值如果Node数组为空则初始化该数组(初始化方法中使用cas操作保险并发安全)根据hash值计算出数组下标如果该下标下的Node为null则创建一个新的插入(使用CAS操作保证并发安全)如果该下标下已经存在了Node元素则判断是链表还是红黑树,分别调用不同的方法来插入或者替换元素(使用synchronized同步代码块来保证并发安全)插入元素完成后如果是链表则需要判断是否需要把链表转为红黑树(链表长度大于等于8则转换为红黑树)最后对Node数组中的Node元素进行统计 public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // 判断key和value是否为null,如果是则抛出异常,说明该Map是不支持null值的 if (key == null || value == null) throw new NullPointerException(); // 根据key计算出hash值 int hash = spread(key.hashCode()); int binCount = 0; // 把Node数组赋值给局部变量tab之后,进入一个死循环 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 如果tab为null则需要初始化该数组 if (tab == null || (n = tab.length) == 0) // 这里对Node数组进行初始化,该方法里会保证线程安全 // 只有一个线程会成功执行数组的初始化,其他线程会直接返回初始化好的数组 // 然后结束本次循环,进入下一次循环,下一次循环判断tab不为null之后就不会再执行到这里了 tab = initTable(); // (n - 1) & hash 计算出数组下标 // tabAt方法为根据下标从内存取出Node数组中的元素 // 如果该下标下的元素为null,则创建一个新的Node元素,使用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 } // 如果取出的f不为空,并且被标记为正在调整大小则进入此分支 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);// 帮助数据转移。 // 如果node数组已经初始化了,并且要插入的下标位置已经有了数据, // 那么就需要对链表或者红黑树进行插入节点或者替换节点的操作 else { V oldVal = null; // 使用synchronized锁,锁对象为该下标下的元素 synchronized (f) { // 拿到锁之后再判断一下这个f对象有没有被其他线程修改 // 如果被修改了的话,就先不做修改,等下次循环到这里的时候再判断能不能改 if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; // 如果是key相同是覆盖操作的话,这里if就成立 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; // 使用尾插发插入元素 if ((e = e.next) == null) { 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; } } } }// 同步代码块结束 // 操作完成之后判断一下是否需要对链表进行转换为树结构 if (binCount != 0) { // 如果链表的长度大于等于8,则把链表转换为树 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i);// 该方法中有同步代码块来解决并发问题 // 如果进入了这个if,则说明之前的put操作成功了 if (oldVal != null)// 如果是替换操作则直接返回旧值 return oldVal; break; } } } // 如果不是替换操作,并且新增元素成功的话就会执行到这里 // 这里会对node的数据进行统计,如果需要扩容则扩容 // binCount,如果为0,说明是在空节点新增 addCount(1L, binCount); return null; }

    2.1 initTable() 初始化Node数组的方法

    使用CAS原子修改sizeCtl的值,修改成功的线程才能进行初始化,只会有一个线程能修改成功,其他线程如果发现该值被修改了就会让出CPU,直到Node数组初始化完成后退出该方法。

    private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 如果sizeCtl小于0则让出cpu(因为已经有线程在进行初始化了) // sizeCtl就是初始容量的大小,把它赋值给局部变量sc,sc记录下了容量 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin // 使用cas操作把sizeCtl的值设置为-1,只会有一个线程能设置成功 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 当其他线程也执行到这里的时候,判断table就不为null,会直接返回初始化好的数组 if ((tab = table) == null || tab.length == 0) { // sc记录下了容量大小,把容量大小赋值给n,如果在new ConcurrentHashMap的时候没有指定容量,那么这里就使用默认的容量16 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];// 创建出Node数组 table = tab = nt;// 将Node数组赋值给table属性 sc = n - (n >>> 2); } } finally { // 初始化之后,保存要调整表大小的下一个元素计数值。 sizeCtl = sc; } break; } } return tab; }

    2.2 addCount(long x, int check) 对node的数据进行统计,如果需要扩容则扩容

    对Node元素+1成功之后(使用fullAddCount方法CAS操作保证并发安全)计算出当前的Node元素个数;

    判断当前元素个数是否达到扩容阈值,如果是则进行扩容;

    /** * 表初始化和调整大小控件。 * 当为负数时,表正在初始化或调整大小:-1表示初始化,否则为-(1 +活动调整大小的线程数)。 * 当表为null时,保存创建时要使用的初始表大小,或默认为0。 * 初始化之后,保存扩容阈值。 */ private transient volatile int sizeCtl; private final void addCount(long x, int check) { CounterCell[] as; long b, s; // 如果单元计数器不为空 或者对基础计数器值原子更新失败时 :if成立 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true;// 默认无竞争 // ThreadLocalRandom.getProbe():返回当前线程的探针哈希值 // 如果单元计数器还没有初始化 或者计数器的长度为0 或者该线程对应的计数元素还没有创建 if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || // CELLVALUE 为CounterCell计数器对象的value值 // 获取对计数器对象的原子+1操作失败 !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 总之就是如果统计未成功就执行到这里,然后使用fullAddCount方法来进行统计 // uncontended:如果原子操作失败则为false,说明有竞争;如果线程计数器对象还没有初始化则默认为true无竞争 fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount();// 计算出当前的数据量 } // 对数组进行扩容 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; //检查当前集合元素个数 s 是否达到扩容阈值 sizeCtl ,扩容时 sizeCtl 为负数,依旧成立, // 同时还得满足数组非空且数组长度不能大于允许的数组最大长度这两个条件才能继续 //这个 while 循环除了判断是否达到阈值从而进行扩容操作之外还有一个作用就是当一条线程完成自己的迁移任务后, // 如果集合还在扩容,则会继续循环,加入迁移任务 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) {// sc < 0 说明集合正在扩容当中 //判断扩容是否结束或者并发扩容线程数是否已达最大值,如果是的话直接结束while循环 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(tab, nt); } //如果集合还未处于扩容状态中,则进入扩容方法,并首先初始化 nextTab 数组,也就是新数组 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } } // See LongAdder version for explanation private final void fullAddCount(long x, boolean wasUncontended) { int h; if ((h = ThreadLocalRandom.getProbe()) == 0) { ThreadLocalRandom.localInit(); // 如果为0则需要对这个值进行初始化 h = ThreadLocalRandom.getProbe();// 获取线程的探针哈希值 wasUncontended = true;// 如果没有初始化则设置为无竞争 } boolean collide = false; // True if last slot nonempty for (;;) { CounterCell[] as; CounterCell a; int n; long v; // 如果单元计数器数组已经初始化过了 if成立 if ((as = counterCells) != null && (n = as.length) > 0) { if ((a = as[(n - 1) & h]) == null) {// 判断该线程对应的计数器元素是否为空 如果为空if成立 if (cellsBusy == 0) { // Try to attach new Cell // 创建计数器元素 CounterCell r = new CounterCell(x); // Optimistic create if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { boolean created = false; try { // Recheck under lock CounterCell[] rs; int m, j; if ((rs = counterCells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { rs[j] = r;// 将计数器元素赋值到指定下标下 created = true;// 为true下面会跳出循环 } } finally { cellsBusy = 0;// 将标记重置为0 } if (created)// 赋值操作成功后created为true,跳出循环 break; continue; // Slot is now non-empty } } collide = false; } else if (!wasUncontended) // CAS already known to fail wasUncontended = true; // Continue after rehash // 直接对计数器元素的值进行+1操作 如果成功则跳出循环 else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) break; else if (counterCells != as || n >= NCPU) collide = false; // At max size or stale else if (!collide) collide = true; // 如果没有其他线程操作 并且原子操作成功(相当于上锁) else if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { try { if (counterCells == as) {// 检查计数器数组有没有被修改过 CounterCell[] rs = new CounterCell[n << 1]; for (int i = 0; i < n; ++i) rs[i] = as[i]; counterCells = rs; } } finally { cellsBusy = 0;// 置为0(相当于释放锁) } collide = false; continue; // Retry with expanded table } h = ThreadLocalRandom.advanceProbe(h); } // 如果数组没有被修改并且上锁成功 此分支成立 else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { boolean init = false; try { // Initialize table if (counterCells == as) {// 再次判断 CounterCell[] rs = new CounterCell[2]; rs[h & 1] = new CounterCell(x); counterCells = rs; init = true; } } finally { cellsBusy = 0;// 置为0(相当于释放锁) } if (init) break; } // 如果对计数器+1成功则跳出循环 else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) break; // Fall back on using base }

    3,get方法

    根据key计算出hash;根据hash计算出数组下标如果是链表则遍厉查找,如果是红黑树则调用find方法进行查找找到则返回结果,没有找到返回null public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; int h = spread(key.hashCode());// 计算出数组下标 // 如果该下标下的元素不为null if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { if ((eh = e.hash) == h) { // 在头节点找到了key相同的Node,则返回node的value值即可 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } else if (eh < 0) // 如果节点的hash值小于0 ,则直接调用node(TreeNode)的查找key节点的方法 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; } } // 如果都没有找到则返回null return null; }

    4,size方法

    调用sumCount方法进行计算

    遍厉CounterCell数组,把多个线程在CounterCell数组中统计的数据进行求和,得到总数

    public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { // 如果单元计数器数组不为空,则遍厉该数组进行统计 for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } // 最终返回统计结果 return sum; }

    5,remove方法

    根据key找到Node数组对应下标的头元素(如果没有找到则直接返回null)对该头元素加锁保证并发安全从链表或者红黑树中找到该key的node数据,记录数据后删除,然后维护链表或者红黑树结构如果红黑树在删除了一个节点之后node数量小于8,则把红黑树转为链表如果删除成功,需要对计数器进行-1操作最后返回被删除的值 final V replaceNode(Object key, V value, Object cv) { // 根据key计算出hash值 int hash = spread(key.hashCode()); for (Node<K,V>[] tab = table;;) {// 死循环 Node<K,V> f; int n, i, fh; // 如果node数组为空,或者该hash对应的数组下标没有数据则跳出循环,最终返回null if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; // 如果该集合当前正在进行扩容数据迁移则帮助迁移,完成后得到新的node数组数据 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; boolean validated = false; // 对hash下标的头元素进行加锁 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; } } // 如果是红黑树,则调用findTreeNode查找元素,记录旧值 // 如果删除数据后node数量小于8则恢复为链表结构 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)); } } } } } // 该key对应的元素存在 if (validated) { if (oldVal != null) { if (value == null) // 对统计数减一 addCount(-1L, -1); return oldVal; } break; } } } return null; }

    三、对比

    1,并发级别

    1.7中使用segment数组来控制并发级别(默认为16) 1.8中使用Node数组的头几点来作为锁对象,并发级别为Node数组的长度,即为Map集合的容量。

    2,数据结构

    1.7中如果一个Node下标对应了多个数据,则这多个数据的数据结构表现为链表

    1.8中如果一个Node下标对应了多个数据,这多个数据的数量如果大于等于8则为红黑树,小于8则为数组。

    3,链表的插入

    1.7使用头插法

    1.8使用尾插法

    4,扩容

    1.7的扩容是对segment对象中的一小段node数组进行扩容

    1.8的扩容是对整个node数组进行扩容,同时支持多线程扩容,并且在扩容的时候没有锁死整个node数组,可以支持部分数据的访问。

     

    Processed: 0.012, SQL: 9