ConcurrentHashMap -1.8 源码解析

ConcurrentHashMap -1.8 源码解析

加锁机制

JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

存储结构

Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,从1.7的 Segment 数组 + HashEntry 数组 + 链表变成了1.8的Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。

img

重要参数

sizeCtl

它的值决定着当前的初始化状态。

  1. -1 说明正在初始化
  2. -N 说明有N-1个线程正在进行扩容
  3. 表示 table 初始化大小,如果 table 没有初始化
  4. 表示 table 容量,如果 table已经初始化。
/*
    该字段控制table(也被称作hash桶数组)的初始化和扩容。
    sizeCtl为负数的时候,表示table初始化或者扩容。
    sizeCtl = -1 表示已经初始化。
    sizeCtl = -(1+正在扩容的线程数)
     */
    private transient volatile int sizeCtl;

	static final int MOVED     = -1; // 表示正在转移(扩容)
	static final int TREEBIN   = -2; // 表示已经转换成树
	static final int RESERVED  = -3; // 表示正在

    //最大容量(table最大容量是2的30次方)
    private static final int MAXIMUM_CAPACITY = 1 << 30;

 
    //默认容量(table默认初始化容量16。扩容总是2的n次方。)
    private static final int DEFAULT_CAPACITY = 16;

    //默认并发级别
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    //默认的负载因子0.75(当前已使用容量 >= 负载因子*总容量的时候,进行resize扩容)
    private static final float LOAD_FACTOR = 0.75f;

    //转红黑树阈值,当桶内链表长度>=8时,会将链表转成红黑树
    static final int TREEIFY_THRESHOLD = 8;

 
    //红黑树还原链表阈值,当桶内node小于6时,红黑树会转成链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //最小树型化容量(table的总容量,要大于64,桶内链表才转换为树形结构,否则当桶内链表长度>=8时会扩容)
    static final int MIN_TREEIFY_CAPACITY = 64;

构造函数

 /**
     使用默认的初始表大小 (16) 创建一个新的空映射。
     */
    public ConcurrentHashMap() {
    }

    /**
    构造函数,其初始表大小可容纳指定数量的元素,而无需动态调整大小。
     @param initialCapacity 初始容量。如果元素的初始容量为负,则抛出异常
     @throws IllegalArgumentException
     */
    public ConcurrentHashMap(int initialCapacity) {
        //如果初始容量为负数抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        //如果初始容量>=最大容量逻辑右移一位就赋值最大容量
         //   否则返回大于输入参数且最近的2的整数次幂的数
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        //赋值给sizeCtl参数
        this.sizeCtl = cap;
    }

initTable()初始化

使用 sizeCtl 中记录的大小初始化表

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;

        // 如果table为空或者长度为0,进入while准备开始初始化。
        while ((tab = table) == null || tab.length == 0) {

            // 将sizeCtl赋值给sc。如果sizeCtl<0说明有线程正在初始化,当前线程要进入等待状态
            if ((sc = sizeCtl) < 0)
                // 线程进入等待
                Thread.yield(); // lost initialization race; just spin

                // 将sizeCtl设置为-1,代表抢到了锁,开始进行初始化操作
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //再次判断表是否为空
                    if ((tab = table) == null || tab.length == 0) {
                        //判断sc实际为(sizeCtl),构造函数时代表了初始化容量
                        //如果有指定初始化容量,就用用户指定的,否则用默认的16.
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")

                        // 生成一个长度为n(上面的容量)的Node数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //将地址赋给table
                        table = tab = nt;

                        // 重新设置sizeCtl=数组长度 - (数组长度 >>>2)
                        // 如果 n 为 16 的话,那么这里 sc = 12
                        // 其实就是 0.75 * 长度(默认的扩容阈值)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 重新设置sizeCtl
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

put操作

 /**
   将指定的键映射到此表中的指定值。键和值都不能为空。

      可以通过使用与原始键相同的键调用 {get 方法来检索该值。
     @param key 与指定值关联的键
     @param value 与指定键关联的值
     @return 与  key 关联的前一个值,如果 key没有映射,则为 null
     @throws NullPointerException 如果指定的键或值为空
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //如果key或者value为空抛出异常
        if (key == null || value == null) throw new NullPointerException();

        // 计算hash 值
        int hash = spread(key.hashCode());

        // 用来记录所在table数组中的桶的中链表的个数,后面会用于判断是否链表过长需要转红黑树
        int binCount = 0;

        //for循环用break跳出
        for (Node<K,V>[] tab = table;;) {

            Node<K,V> f; int n, i, fh;

            // 如果数组"空",进行数组初始化
            if (tab == null || (n = tab.length) == 0)
                // 初始化table
                tab = initTable();

             //i为下标,用(数组长度-1)&hash值计算得出
             //调用tabAt()获取数组中该下标对应的元素
            //如果这位置为空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //    使用CAS 操作将这个新值(将新值放入结点,再将结点放入期中)即可
                //          如果 CAS 失败,那就是有并发操作,继续循环
                if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果头结点hash值为-1,则为ForwardingNode结点,说明正在扩容
            else if ((fh = f.hash) == MOVED)
                // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
                tab = helpTransfer(tab, f);

            else { // 到这里就是说,f 是该位置的头结点,而且不为空

                V oldVal = null;
                // 获取数组该位置的头结点的监视器锁,锁住头结点
                synchronized (f) {

                    //双重检测锁,检测加锁前是否被修改
                    if (tabAt(tab, i) == f) {

                        if (fh >= 0) { // 头结点的 hash 值大于 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;
                                }
                                // 没发现相等的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) { // 红黑树
                            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;
                            }
                        }
                    }
                }
                //如果链表的长度不为0
                if (binCount != 0) {
                    // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
                        // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        //返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        // 计数器加1,完成新增后,table扩容,就是这里面触发
        addCount(1L, binCount);

        //新增后返回空
        return null;
    }

扩容

两个方法调用扩容

1.每次添加完后,调用的addCount中有调用transfer扩容

2.桶中链表大于8调用treeifyBin方法转红黑树的方法的时候,在该方法中会判断table当前总容量是否大于64,如果table当前总容量小于64,不会转红黑树,而是调用tryPresize方法尝试扩容,tryPresize方法中会调用transfer扩容

扩容怎么保证线程安全

1.多个线程都做扩容的时候,由字段transferIndex表示当前已分配的桶到什么下标了,对transferIndex字段的修改是用的CAS,每个线程先获取自己处理哪个区间的桶,每个线程自己迁移自己的桶,互不打扰。一个线程最少处理16个桶。

比如,现在数组长度为32,线程A迁移0-15的桶,线程B迁移16-31的桶。当前哪些区间的桶被分配的的临界值是transferIndex表示,对它的修改是CAS的,所以多线程扩容线程安全

2.如果有线程去写concurrenthashmap,发现现在正在扩容,则去帮组扩容。如果有线程去读,发现正在扩容,则通过桶上的forwdingNode去新的map中去读。

源码

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;

        // stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16(每个做扩容的线程至少处理16个桶)
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range

        //如果nextTab为空,新建一个是原来2倍长度的nextab
        if (nextTab == null) {
            try {
                // 容量翻倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            // nextTable 是 ConcurrentHashMap 中的属性
            nextTable = nextTab;
            // transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
            transferIndex = n;
        }

        int nextn = nextTab.length;

        // ForwardingNode 翻译过来就是正在被迁移的 Node
        // 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
        // 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
        //    就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
        //    所以它其实相当于是一个标志。
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);


        // advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
        boolean advance = true;
        //所有桶是否都已迁移完成
        boolean finishing = false; // to ensure sweep before committing nextTab

        /*
         * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
         *
         */

        // i 是位置索引,bound 是边界,注意是从后往前
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;

            //这个while是给当前线程分配迁移任务,即它负责迁移哪几个桶,它要处理的桶的下标范围
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;

                //用CAS设置transfer减去已分配的桶,并发扩容保证线程安全,每个扩容的线程根据这个字段扩容自己分配到区间的桶,各不干扰
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    // /确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    // 所有的迁移操作已经完成
                    nextTable = null;
                    // 将新的 nextTab 赋值给 table 属性,完成迁移
                    table = nextTab;
                    // 重新计算 sizeCtl: n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }

                // 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
                //当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;

                    // 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
                    // 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
                // 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        // 头结点的 hash 大于 0,说明是链表的 Node 节点
                        if (fh >= 0) {
                            // 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
                            // 需要将链表一分为二,
                            //   找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
                            //   lastRun 之前的节点需要进行克隆,然后分到两个链表中
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // 低位链表放在i处
                            setTabAt(nextTab, i, ln);
                            // 高位链表放在i+n处
                            setTabAt(nextTab, i + n, hn);

                            // 将原数组该位置处设置为ForwardingNode,代表该位置已经处理完毕,
                            // 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                            setTabAt(tab, i, fwd);
                            // advance 设置为 true,代表该位置已经迁移完毕
                            advance = true;
                        }
                        // 红黑树的迁移
                        else if (f instanceof TreeBin) {

                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            // 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;

                            // 将 ln 放置在新数组的位置 i
                            setTabAt(nextTab, i, ln);
                            // 将 hn 放置在新数组的位置 i+n
                            setTabAt(nextTab, i + n, hn);
                            // 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
                            //    其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
                            setTabAt(tab, i, fwd);
                            // advance 设置为 true,代表该位置已经迁移完毕
                            advance = true;
                        }
                    }
                }

treeifyBin()链表转红黑树

 private final void treeifyBin(Node<K,V>[] tab, int index) {
 
        // b表示需要转换为红黑树的那个桶在数组中的下标
        Node<K,V> b; int n, sc;
 
        // 如果table不为空
        if (tab != null) {
 
            // 如果table长小于64,调用tryPresize扩容,而不是转换为红黑树
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
 
                // 调用tryPresize扩容
                tryPresize(n << 1);
 
            // 开始进行转换为红黑树
            // 得到要转换为红黑树的链表的头节点,如果头节点不为空,并且头节点的hash >= 0 
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
 
                // 锁住头节点
                synchronized (b) {
 
                    // 双重锁检查,以防在锁之前又被其他线程改变了该桶头节点的内容
                    if (tabAt(tab, index) == b) {
 
                        // hd表示红黑树的根节点
                        // tl表示preNode
                        TreeNode<K,V> hd = null, tl = null;
 
                        // 遍历链表
                        for (Node<K,V> e = b; e != null; e = e.next) {
 
                            // 把链表中的每个Node包装为TreeNode
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
 
                                // 确定红黑树的根节点
                                hd = p;
                            else
                                // 还是要维护next指针
                                tl.next = p;
                            tl = p;
                        }
 
                        //用TreeBin<K,V>包装红黑树的根节点,并放入到数组的桶中
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

get操作

**get操作是无锁的。**即使TreeBin的find函数有可能会加TreeBin的内部读锁,但也是非阻塞的。

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

        // 得到key的哈希值
        int h = spread(key.hashCode());
        // 如果tabele不为空,并且tab.length大于0,得到桶的头节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {

            // 桶的头节点的哈希值等于要get的key的哈希值
            if ((eh = e.hash) == h) {

                //桶的头节点的key等于要get的key
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    //那么桶的头节点就是我们要get的节点,直接返回头节点的value
                    return e.val;
            }

            // 桶的头节点的哈希值小于0,表示在红黑树上或者正在扩容
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;

            // 这里表示在桶的链表上
            // 遍历该桶的链表找到get的节点,返回节点的value
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

这里可以看到get方法是没有加锁的。Node中的value和nextNode定义的时候用了volatile来保证可见性和有序性

ConcurrentHashMap的同步机制

核心:

  1. 读读不互斥

  2. 读写不互斥

  3. 写写互斥

5.1 读读不互斥

可以看到整个get方法是没有锁的,无论是synchronized所还是JUC包中的那些Lock,都没有。

读方法细分:

  1. 桶内只是链表,直接遍历链表读了。无任何锁性质的东西。

  2. 桶内有红黑树,在TreeBin的find方法中操作,是读读同时进行的时候,用红黑树查找。这里用CAS设置LockState字段,不要去理解成成读读互斥了,并不是一个线程读完了才能让另一个线程读,是只有把lockState字段增加这个操作本身互斥而已。

举个例子:

两个线程同时读,A线程用CAS设置了LockState字段为读后,A开始真的做读操作。B线程并不需要等A读完才能读,B线程只需要等A设置完LockState字段后,自己就能去设置LockState字段了然后开始读了。

5.2 读写不互斥

虽然写方法put会用synchronized去锁桶内头节点/红黑树的根节点

但是:读方法get没有任何锁性质的东西,不需要获取桶内头节点的synchronized锁

读方法细分:

1.桶内只是链表,直接遍历链表读了。无任何锁性质的东西

2.桶内有红黑树,在TreeBin的find方法中操作,是读写同时进行的时候,用链表方式查找

5.3 写写互斥

写和写并发的时候肯定是互斥的一个线程在写的时候用synchronized对桶内头节点/红黑树的根节点加锁另一个线程要写同一个桶,首先要用synchronized获取锁,此时只有等待,等正在写的线程写完后释放锁,再去竞争资源。


ConCurrentHashMap在1.7和1.8区别

ConCurrentHashMap 1.8 相比 1.7的话,主要改变为:

  • 去除 Segment + HashEntry + Unsafe 的实现,改为 Synchronized + CAS + Node + Unsafe 的实现,其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。
  • Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了在加锁。
  • put()方法中 初始化数组大小时,1.8不用加锁,因为用了个 sizeCtl 变量,将这个变量置为**-1**,就表明table正在初始化

下面简单介绍下主要的几个方法的一些区别:

1. put() 方法

JDK1.7中的实现:

ConCurrentHashMap 和 HashMap 的put()方法实现基本类似,所以主要讲一下为了实现并发性,ConCurrentHashMap 1.7 有了什么改变

  • 需要定位 2 次 (segments[i],segment中的table[i])
    1. 先通过key的 rehash值的高位segments数组大小-1 相与得到在 segments中的位置
    2. 然后在通过 key的rehash值table数组大小-1 相与得到在table中的位置
  • 没获取到 segment锁的线程,不能进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:
    1. 计算table[i]的位置(你的值要put到哪个桶中)
    2. 通过首节点first遍历链表找有没有相同key
    3. 在进行1、2的期间最多自旋64次获取锁,超过则线程挂起

JDK1.8中的实现:

    1. 先拿到根据rehash值定位,拿到table[i]的首节点first,然后:
      • 如果为 null ,通过 CAS 的方式把 value put进去
      • 如果 非null ,并且 first.hash == -1 ,说明其他线程在扩容,参与一起扩容
      • 如果 非null ,并且 first.hash != -1 ,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。
2. get() 方法

JDK1.7中的实现:

  • 由于变量 value 是由 volatile 修饰的,java内存模型中的 happen before 规则保证了 对于 volatile 修饰的变量始终是 写操作 先于 读操作 的,并且还有 volatile内存可见性 保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。
  • 如果get()到的是null值才去加锁。

JDK1.8中的实现:

  • 和 JDK1.7类似
3. resize() 方法

JDK1.7中的实现:

  • 跟HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容(1.new个2倍数组 2.遍历old数组节点搬去新数组)。

JDK1.8中的实现:

  • 支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点(ForwardingNode),这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。
4. 计算size

JDK1.7中的实现:

  • 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。
  • 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的 Count 的和

JDK1.8中的实现:

由于没有segment的概念,所以只需要用一个 baseCount 变量来记录当前节点的个数

  1. 先尝试通过CAS 修改 baseCount
  2. 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将 CELLSBUSY 置1,成功则可以把 baseCount变化的次数 暂存到一个数组 counterCells 里,后续数组 counterCells 的值会加到 baseCount 中。
  3. 如果 CELLSBUSY 置1失败又会反复进行CASbaseCount 和 CAScounterCells数组
  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值