ConcurrentHashMap源码解析

源码解读

1.7底层实现原理

Segment对象

继承ReentrantLock重入锁

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
        transient volatile HashEntry<K,V>[] table;
        transient int count;
        transient int modCount;
        transient int threshold;
        final float loadFactor;
    }

默认的构造函数

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        //initialCapacity=16,loadFactor=0.75 ,concurrencyLevel=16
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //concurrencyLevel 默认值是16  相当于16把锁
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        int sshift = 0;
        //segments数组的容量
        int ssize = 1;
        //循环完成之后 sshift=4  ssize=16
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        //28 计算index右移动28位
        this.segmentShift = 32 - sshift;
        //与运算的时候均匀的存放到每个segment对象
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //表示HashEntry<K,V>[] table初始容量
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        //默认为2
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        //创建一个s0对象,目的是方便后期其他key落到不同的segment中,能够创建segment对象的加载因子,初始化容量的大小
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
            //创建segment数组容量 默认容量大小为16
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

put方法

    public V put(K key, V value) {
        Segment<K,V> s;
        //空的value就报错
        if (value == null)
            throw new NullPointerException();
        //计算hash值
        int hash = hash(key);
        //segmentShift=28 segmentMask=15,初始化的时候设置的,计算在segments数组中的索引下标位置
        int j = (hash >>> segmentShift) & segmentMask;
        //获取对应的segments
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
            //把对应的key和value放到对应的segemnts下面的HashEntry数组中
        return s.put(key, hash, value, false);
    }

        private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        //如果对应的索引下标的segment对象为空
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //则拿到s0模板参数
            Segment<K,V> proto = ss[0]; 
            //segement对象中的HashEntry数组的容量
            int cap = proto.table.length;
            //加载因子
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            //创建hashEntry对象
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                //再次校验是否为空,如果不为空则创建新的segement对象
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    //最后一次校验是否存在,如果不存在则把上面创建的segemnt对象放到segments数组中
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        //获取对应的segment对应的锁 如果获取都为true,node为空
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                //计算在HashEntry数组中的索引下标位置
                int index = (tab.length - 1) & hash;
                //获取对应索引下标位置的值
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    //对应索引下标的值不为空
                    if (e != null) {
                        K k;
                        //如果key相同则修改
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    //如果之前不存在该key,并且已经遍历到链表的最后一个节点
                    else {
                        //node不为空则采用头插法直接赋值
                        if (node != null)
                            node.setNext(first);
                        else
                            //node为空 则创建新的HashEntry对象,并且采用的是头插法
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //是否HashEntry数组需要扩容,注意Segments数组一旦初始化完成之后就不能修改了,不支持扩容,只是segment对象中的HashEntry对象支持扩容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
//比如first=a ,然后线程1要进行赋值b修改,线程2要加进行赋值c,刚好在同一个segment对象下面HashEntry数组中的索引位置是2
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    //对应索引下标位置,如index=2,first=a
            HashEntry<K,V> first = entryForHash(this, hash);
            //e=a
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; 
            //不断的循环获取锁
            while (!tryLock()) {
                HashEntry<K,V> f; 
                //先进行遍历index=2的链表,
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) 
                            //遍历链表到最后节点之后进行创建新的节点
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    //如果是同一个key则设置retry次数
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        //遍历链表
                        e = e.next;
                }
                //MAX_SCAN_RETRIES最大值是64 如果超过64则该线程进入阻塞状态,不进行自旋去获取锁
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                     //如果能进来,说明现在first节点跟之前缓存的不一样,之前缓存的是a,有线程把first节点修改了,现在线程1把它改成b,b→a,所以需要重新赋值头节点和修改retries次数
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

总结

  • Segments数组一旦被初始化之后就不支持扩容,是每个segment中的HashEntry数组存在扩容的情况。
  • 分段锁有多个segment对象组成,每个segment对象继承ReentrantLock,使用了lock锁
  • 使用了cas做修改

1.8底层原理

  • 去除Segment分段锁
  • 去除lock锁,采用了synchronized(jdk1.6之后进行了优化支持自旋以及锁的升级)+cas进行保证并发下的线程同步
  • 数据结构是数组+链表+红黑树
  • 对index下标对应的node节点上锁
  • 锁的竞争,多个线程put key的时候多个key的index都相同的时候,落入到同一个node节点的时候,需要做锁的竞争,如果index不同的时候,落入到不同的node节点的时候,不需要做锁的竞争。

        private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //sizeCtl默认值是0,用来控制table的初始化和扩容操作,如果为-1表示有一个线程在扩容,-2表示两个线程在扩容
            if ((sc = sizeCtl) < 0)
                //放弃该线程的优先执行权
                Thread.yield(); 
            //使用cas进行自旋
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    //初始化node数组的大小 默认为16
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //不支持key和value为空
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        //如果大于8链表就转红黑树
        int binCount = 0;
        //自旋
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //初始化为空,则初始化数组
            if (tab == null || (n = tab.length) == 0)
                //初始化node数组
                tab = initTable();
            //计算对应的index下标位置
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //没有发生index冲突,使用cas去修改,如果修改成功则退出循环,如果没有修改成功说明有线程同时在该index进行修改,继续进行自旋
                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 {
                //如果当前索引下标发生index冲突了,则使用synchronized上锁
                V oldVal = null;
                //f为node节点
                synchronized (f) {
                    //判断头节点有没有发生改变
                    if (tabAt(tab, i) == f) {
                        if (fh >= 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<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;
                            }
                        }
                    }
                }
                //判断当前节点的链表长度是否大于8 如果大于8就转红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //计算节点的数量
        addCount(1L, binCount);
        return null;
    }
        private final void addCount(long x, int check) {
        //采用了CounterCell来缓存各个node节点的数量,最后在统一算整个节点的数量,这样能够减少在并发情况下,对size++进行不断的自旋减少开销
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.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);
                if (sc < 0) {
                    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);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

CounterCell对象的作用

  • 在并发情况下,如果要统计整个ConcurrentHashMap的节点的数量,对size++采用cas不断的自旋操作的时候会占用大量的cpu
  • 通过使用CounterCell对象来记录每个node节点的数量,最后再统一遍历计算整个ConcurrentHashMap的数量

JDK1.8去除分段锁的原因

  • 因为jdk1.7的分段锁要计算两次index值,第一次计算在segement数组中的索引位置,第二次计算在segement对象中的HashEntry中数组的位置
  • 分段锁一旦初始化之后锁的数量就不会发生改变了,jdk1.8随着node数组的扩大,锁的数量也会增加
  • jdk1.8只需要计算一次index值,在node数组中的哪个索引位置即可

ConcurrentHashMap使用synchronized锁的作用而不是lock锁的原因

  • synchronized锁在jdk1.6之后进行了优化,支持自旋以及锁的升级过程(偏向锁、轻量级锁、重量级锁)
  • lock锁的自旋过程是不支持自旋的,需要写大量的代码进行维护

jdk1.8锁的总结

  • node节点没有发生index冲突的时候采用cas锁
  • node节点发生index冲突采用synchronized锁
    参考:蚂蚁课堂
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值