ConcurrentHashMap 1.8

总体结构(Node数组+链表+红黑树)

image.png

构造方法

  1. 初始化容量不能小于并发等级
  2. 1.7中的Segment为创建即加载,并初始化0下标的HashEntry,在1.8中改进为懒加载,初始化时仅进行Node数组长度的计算
  3. 长度的计算方式是 (1.0 + (long)initialCapacity / loadFactor) 大于等于该数的2次方数
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;
}

Get方法

  1. 在1.8的构造中采用Node代替Segment,在获取数据的时候不需要通过 UNSAFE.getObjectVolatile保证可见性
  2. 正常的所有的hash值都会被设定为正数
  3. 对于正在扩容的节点会赋值为-1,树节点会为-2,因此在判断出该节点hash值为负数时,则说明不是正常的链表结构,需要使用find进行查询
  4. 如果是链表结构则直接进行遍历获取
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // spread 方法能确保返回结果是正数
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果头结点已经是要查找的 key
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 正常遍历链表, 用 equals 比较
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

Put

  1. 在元素没有真正put成功前,会一直死循环,首先会计算出key对应的hash下标
  2. 首先会通过自旋+CAS的方式创建数组桶,只有第一个通过CAS操作将sizectl改为-1的才说明获取到锁,-1也就代表正在进行初始化或扩容操作,-N则说明有N个线程正在进行操作,其他没有获取到锁的线程则会调用yield()方法让出时间片,让其他线程能够以更高的效率进行操作,在桶创建成功后再通过CAS将元素加入到桶中
  3. 如果桶中的元素为空,则直接CAS放入即可,不需要进行加锁操作,退出put方法
  4. 如果此时判断出节点的Hash值为-1,说明此时旧数组桶正在进行扩容,并且旧数组桶的该下标已经扩容完毕,此时该线程会帮忙进行扩容流程
  5. 当数组桶创建好,并且没有在扩容状态中,并且此时出现Hash冲突,此时会进行Synchronized加锁操作,此时只会对该Node节点进行加锁操作
  6. 如果该桶对应的是链表,则直接遍历,如果有相同的节点则进行覆盖操作,没有则会使用尾插法插入到该链表的末尾
  7. 否则说明是红黑树,则会执行对应的红黑树添加节点流程
  8. 添加节点完毕后,如果是链表节点会记录出链表的长度,如果链表长度达到8,则会将链表转为红黑树,红黑树的转换操作会进行Synchronized加锁操作,锁住头节点
  9. 当所有流程结束后,会对map中记录了所有节点的值进行添加操作,类似于原子类
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 不能为空
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // f = 目标位置元素
        Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
        if (tab == null || (n = tab.length) == 0)
            // 数组桶为空,初始化数组桶(自旋+CAS)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
            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)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 使用 synchronized 加锁加入节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 说明是链表
                    if (fh >= 0) {
                        binCount = 1;
                        // 循环加入新的或者覆盖节点
                        for (Node<K,V> 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<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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield();
        // 尝试将 sizeCtl 设置为 -1(表示初始化 table)
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab; 
}

size计数

  1. size的计算会在put和remove的过程中进行统计
  2. 如果没有竞争,则会向basecount进行计数
  3. 如果有竞争发生,则会记录在countersCells

扩容过程

  1. 通过CPU核数的计算,计算出每个线程要迁移多少个桶,每个线程要迁移的桶是平均的
  2. 计算出扩容后的数组大小,即原数组的两倍
  3. 每个线程最少处理16个桶的扩容,通过CAS操作为每个节点分配任务,即初始值和长度
  4. 处理过的节点会将节点标识设置为forward即标志位-1
private final void addCount(long x, int check) {
    CounterCell[] cs; long b, s;
    if ((cs = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell c; long v; int m;
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            if (sc < 0) {
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break;
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

/**
 * 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) << RESIZE_STAMP_SHIFT;
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                transferIndex <= 0)
                break;
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

为什么使用Synchronized代替ReentrantLock

锁的粒度

  • 在JDK7中使用的是**Segment+Entry**的组合,Segment是继承自**ReentrantLock**的,在进行Put操作时,需要先**获取到Segment锁**才能进行操作,即锁住整个Segment
  • 并且在JDK7中的Segment在**初始化后就固定**了,在数据量大的情况下,Segment内会包含许多的Entry节点,在这些Entry节点需要进行put操作时,都需要获取Segment锁,此时需要频繁的等待锁
  • 在JDK8中优化为Node,在进行Put操作时需要获取到**链表头节点Node**的Synchronized锁,即**锁住Node节点所在的链表**
  • JDK8中**缩小了锁的粒度**,也就减少了锁竞争的情况,因此JDK8的并发效率会强于JDK7

锁的效率

  • 因为JDK8中的锁粒度在于链表头结点,要远小于JDK7中的Segment,因此其实在JDK8中出现锁竞争的情况已经较少。
  • Synchronized经过有**偏向锁、自旋锁等优化**后,性能已经比以前好很多,此时JDK8中的情况属于锁竞争较小的情况,有比较大的可能可以通过自旋的方式,**在重试数十次后**获取到锁,而不需要升级为重量级锁。
  • ReentrantLock在第一次没有获取到锁后,在新增节点后只会再尝试获取一次锁,如果**两次获取锁失败就会被 LockSupport.park()挂起**,挂起操作与Synchronized获取重量级锁一样,需要切换到内核态,是十分耗费时间的。
  • 因此基于JDK8中基于Node的情况并发量小的情况,采用Synchronized更合适

为什么ConcurrentHashMap不允许有null

  • **ConcurrentHashMap**的put方法中,会判断key和value是否为null,如果为**null会抛出空指针异常**
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
  • 在HashMap中是**允许存储**有null值的,允许有**一个key**为null,允许有**多个value**为null
  • ConcurrentHashMap不允许存储null值的原因是,**不允许存在二义性**,在get方法返回null后,无法判断是该value为null,还是该key不存在
  • HashMap是线程不安全的,默认在**单线程环境**下执行,HashMap在get方法返回null后,可以通过**containsKey**的返回值判断该null值是存在还是不存在。HashMap默认key为null的hash值为0
  • 但是ConcurrentHashMap是线程安全的集合,在get方法返回null后,再通过containsKey判断是否存在时,可能在中间时间会有其他线程对该key进行修改,返回的结果不一定是当时真实的结果,因此会产生二义性。
  • 为了避免这种二义性存在,因此ConcurrentHashMap不允许存储null值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值