ConcurrentHashmap源码分析

ConcurrentHashmap基于JDK1.8

参数

	// node数组最大容量:2^30=1073741824
    private static final int MAXIMUM_CAPACITY = 1 << 30;

	// 默认初始值,必须是2的幕数
    private static final int DEFAULT_CAPACITY = 16;

	//数组可能最大值,需要与toArray()相关方法关联
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

	//并发级别,遗留下来的,为兼容以前的版本
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

	// 负载因子
    private static final float LOAD_FACTOR = 0.75f;

	// 链表转红黑树阀值 > 8 链表转换为红黑树
    static final int TREEIFY_THRESHOLD = 8;

	//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
    static final int UNTREEIFY_THRESHOLD = 6;

	//最小的红黑树容量
    static final int MIN_TREEIFY_CAPACITY = 64;

	//每个传输步骤的最小重组次数。范围被细分以允许多个调整大小线程。此值用作下限,以避免调整大小时遇到过多的内存争用。该值应至少为 DEFAULT_CAPACITY。
    private static final int MIN_TRANSFER_STRIDE = 16;

	//sizeCtl 中用于生成标记的位数。对于 32 位数组,必须至少为 6
    private static int RESIZE_STAMP_BITS = 16;

	// 2^15-1,help resize的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

	// 32-16=16,sizeCtl中记录size大小的偏移量
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

	// forwarding nodes的hash值 如果hash值等于-1代表线程协助扩容 
	static final int MOVED     = -1; // hash for forwarding nodes
	// 树根节点的hash值 如果hash等于-2代表,当前桶是红黑树 s
    static final int TREEBIN   = -2; // hash for roots of trees
	// ReservationNode的hash值
    static final int RESERVED  = -3; // hash for transient reservations
	// 可用处理器数量
    static final int NCPU = Runtime.getRuntime().availableProcessors();

	//存放node的数组
    transient volatile Node<K,V>[] table;

	//下一个要使用的表;仅在调整大小时为非空。
    private transient volatile Node<K,V>[] nextTable;

	//基本计数器值,主要在没有争用时使用,但也可作为表初始化竞赛期间的后备。通过 CAS 更新。
    private transient volatile long baseCount;

	//控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 	//当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 	//当为0时:代表当时的table还没有被初始化
 	//当为正数时:表示初始化或者下一次进行扩容的大小
    private transient volatile int sizeCtl;

	//调整大小时要拆分的下一个表索引(加一个)。
    private transient volatile int transferIndex;

	//调整大小和/或创建 CounterCell 时使用自旋锁(通过 CAS 锁定)。
    private transient volatile int cellsBusy;

	//如果使用CAS计算失败,也就是说当前处于高并发的情况下,那么
    //就会使用CounterCell[]数组进行计数,类似jdk1.7分段锁的形式,锁住一个segment
    //最后size()方法统计出来的大小是baseCount和counterCells数组的总和 作者:牛哄哄的java大师 
    private transient volatile CounterCell[] counterCells;

构造器

//没有维护任何变量的操作,如果调用该方法,数组长度默认是16
public ConcurrentHashMap() {
}

//调用四个参数的构造
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

//传递进来的一个初始化容量,会给予这个值计算一个比这个值大的2的次幂的数作为初始容量
//注意,调用这个方法,得到的初始容量和我们之前讲的HashMap以及jdk7的ConcurrentHashMap不同,即使你传递的是一个2的幂次方数,该方法计算出来的初始容量依然是比这个值大的2的幂次方数
//例如 new ConcurrentHashMap(32);  初始化的容量为64 这与hashmap和1.7的ConcurrentHashMap不同
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

//计算一个大于或者等于给定的容量值,该值是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;
}

//基于一个Map集合,构建一个ConcurrentHashMap
//初始容量为16
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

添加元素

put()

put()方法的流程大致跟hashmap相同,不同的地方是,ConcurrentHashMap在添加元素的时候,会针对当前元素所对应的桶位进行加锁操作,保证元素添加的时候,多线程的安全,同时对某个桶位加锁不会影响其他桶位的操作,进一步提升了多线程的并发效率。

多线程协助扩容的操作会在两个地方被触发:
1、当添加元素时,发现添加的元素对用的桶位为fwd节点,就会先去协助扩容,然后再添加元素
2、当添加完元素后,判断当前元素个数达到了扩容阈值,此时发现sizeCtl的值小于0,并且新数组不为空,这个时候,会去协助扩容

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) {
    //如果有空值或者空键就抛出异常
    if (key == null || value == null) throw new NullPointerException();
    //基于key计算hash值,并进行一定的扰动
    int hash = spread(key.hashCode());
    //这个是记录这个桶的元素个数,目的是用它来判断是否需要转换红黑树
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果数组还未初始化,先对数组进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //如果计算得到的对应数组的下标为空,利用cas将元素添加
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //cas+自旋(和外侧的for构成自旋循环),保证元素添加安全
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //如果hash计算得到的数组下标位置的元素的hash值为MOVED(-1),证明正在扩容,那么就协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //待插入的位置不为空,且当前没有处于扩容操作,就进行元素添加
        else {
            V oldVal = null;
            //对当前数组下的位置进行加锁,保证线程安全,执行添加元素操作
            synchronized (f) {
                //取出要存储的位置的元素,跟前面取出来的比较
                if (tabAt(tab, i) == f) {
                    //表明是链表结点类型,hash值是大于0的,即spread()方法计算而来
                    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;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //链表长度大于/等于8,有可能将链表转成红黑树,因为在treeifyBin(tab, i);
                //方法中还有一个判断数组长度是否小于64的判断,如果小于64,就不会树化。只是数组扩容。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                //如果旧值不为空,则返回旧值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //添加的是新元素,维护集合长度,并判断是否要进行扩容操作
    addCount(1L, binCount);
    return null;
}    

initTable()

//初始化方法
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //判断数组是否为空
    while ((tab = table) == null || tab.length == 0) {
        //如果sizeCtl的值小于0,说明此时正在初始化,让出cpu
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //cas修改sizeCtl的值为-1,修改成功,进行数组初始化,失败了,就继续自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //再次确认数组长度是否为空
                if ((tab = table) == null || tab.length == 0) {
                    //sc为0,取默认长度16,否则去sizeCtl的值
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //进行初始化
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //计算扩容阈值,并赋值sc
                    sc = n - (n >>> 2);
                }
            } finally {
                //将扩容阈值赋值给sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

helpTransfer()

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

addCount()

addCount方法
1、CounterCell数组不为空,优先利用数组中的CounterCell记录数量
2、如果数组为空,尝试对baseCount进行累加,失败后,会执行fullAddCount逻辑
3、如果是添加元素操作,会继续判断是否需要扩容

//计算集合的size
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //当CounterCell数组不为空,则优先利用数组中的CounterCell记录数量 简称:计数盒子
    //或者当baseCount的累加操作失败,会利用数组中的CounterCell记录数量
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        //标识是否有多线程竞争
        boolean uncontended = true;
        //当as数组为空(尚未出现并发)
        //或者当as长度为0
        //或者当前线程对应的as数组桶位的元素为空
        //或者当前线程对as数组桶位的变量进行修改,但是修改失败(出现并发了)
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            //死循环把x记录到counterCells,uncontended是判断有没有尝试用CAS更新过
            fullAddCount(x, uncontended);
            return;
        }
        //到这表示CAS把值记到counterCells肯定成功了,因为上面的是 ||,CAS是最后一个判断条件
        //check == 0:表示插入的下标是空的,此时是头节点,此坑位还有不少直接return
        //check == 1:表示插入的是链表的第2个,此坑位还有不少,就不考虑扩容,直接return
        //check == -1:表示的是删除,直接return
        //这里跟hashMap有点不一样,没有直接判断大于多少就扩容
        if (check <= 1)
            return;
        //把counterCells的值累加到baseCount
        s = sumCount();
    }
    //check>=0表示需要进行扩容操作
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        //当元素个数达到扩容阈值
        //并且数组不为空
        //并且数组长度小于限定的最大值
        //满足以上所有条件,执行扩容
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //根据 length 得到一个标识
            int rs = resizeStamp(n);
            //sizeCtl小于0,说明正在执行扩容,那么协助扩容
            if (sc < 0) {
                //扩容结束或者扩容线程数达到最大值或者扩容后的数组为null或者没有更多的桶位需要转移,结束操作
                // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc == rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                // 如果 nextTable == null(结束扩容了)
                // 如果 transferIndex <= 0 (转移状态变化了)
                // 结束循环
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                //扩容线程加1,成功后,进行协助扩容操作
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    //协助扩容,newTable不为null
                    transfer(tab, nt);
            }
            //没有其他线程在进行扩容,达到扩容阈值后,给sizeCtl赋了一个很大的负数
            //1+1=2 --》 代表此时有一个线程在扩容

            //rs << RESIZE_STAMP_SHIFT 是一个很大的负数
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                //扩容,newTable为null
                transfer(tab, null);
            s = sumCount();
        }
    }
}   

fullAddCount()

fullAddCount方法
1、当CounterCell数组不为空,优先对CounterCell数组中的CounterCell的value累加
2、当CounterCell数组为空,会去创建CounterCell数组,默认长度为2,并对数组中的CounterCell的value累加
3、当数组为空,并且此时有别的线程正在创建数组,那么尝试对baseCount做累加,成功即返回,否则自旋

 private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
    	//获取当前线程hash值
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            //把标志位置为true,确保为true,也就是确保就是没有CAS过
            wasUncontended = true;
        }
    	//标识是否有冲突,如果最后一个桶不是null,那么为true
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //数组不为空,优先对数组中CouterCell的value累加
            if ((as = counterCells) != null && (n = as.length) > 0) {
                //线程对应的桶位不为null
                if ((a = as[(n - 1) & h]) == null) {
                     //cellsBusy为0,表示没线程在初始化或者扩容counterCells
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        //创建CounterCell对象
                        CounterCell r = new CounterCell(x); // Optimistic create
                        //利用CAS修改cellBusy状态为1,,说明线程在用
                        //成功则将刚才创建的CounterCell对象放入数组中
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                //桶位为空, 将CounterCell对象放入数组
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    //表示放入成功
                                    created = true;
                                }
                            } finally {
                                //最终将cellsBusy置为0
                                cellsBusy = 0;
                            }
                            //成功退出循环
                            if (created)
                                break;
                            //桶位已经被别的线程放置了已给CounterCell对象,继续循环
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //桶位不为空,重新计算hash值,然后继续循环
                else if (!wasUncontended)       // CAS already known to fail
                    //进到里面,说明之前在addCount已经CAS过了,并且失败了
                    //第一次只把标志位改为true,改个状态出去,给个机会,第二次再来CAS,避免竞争
                    wasUncontended = true;      // Continue after rehash
                //走到这,2种可能
                //1:是死循环的第二次了
                //2:是之前没有CAS过
                //不管哪种情况,必须要CAS了,要么是第一次,要么已经让过一次了,只能竞争了。
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                //数组被别的线程改变了,或者数组长度超过了可用cpu大小
                //重新计算线程hash值,否则继续下一个判断
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                //当没有冲突,修改为有冲突,并重新计算线程hash,继续循环
                else if (!collide)
                    //进到里面,说明需要扩容了
                    collide = true;
                //如果CounterCell的数组长度没有超过cpu核数,对数组进行两倍扩容
            	//并继续循环
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //每次不成功就算下新的hash,换个下标试试
                h = ThreadLocalRandom.advanceProbe(h);
            }
            //CounterCell数组为空,并且没有线程在创建数组,修改标记,并创建数组
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        //初始化长度为2
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        //设置初始化状态init为true
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //数组为空,并且有别的线程在创建数组,那么尝试对baseCount做累加,成功就退出循环,失败就继续循环
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

transfer()

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //如果是多cpu,那么每个线程划分任务,最小任务量是16个桶位的迁移
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    //如果是扩容线程,此时新数组为null
    //如果不是扩容线程,此时数组不为空。即表示当前线程是协助扩容的线程
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            //两倍扩容创建新数组
            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 = nextTab;
        //记录线程开始迁移的桶位,从后往前迁移
        transferIndex = n;
    }
    //记录新数组的长度
    int nextn = nextTab.length;
    //fwd 节点,当某个桶位数据处理完毕后,将此桶位设置为fwd节点
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    //推进标记
    boolean advance = true;
    //完成标记
    boolean finishing = false; // to ensure sweep before committing nextTab
    //i记录当前正在迁移桶位的索引值
    //bound记录下一次任务迁移的开始桶位
    for (int i = 0, bound = 0;;) {
        //f 桶位的头结点
        //fh 头结点的hash
        Node<K,V> f; int fh;
        while (advance) {
            //分配任务的开始下标
            //分配任务的结束下标
            int nextIndex, nextBound;
            //--i >= bound 成立表示当前线程分配的迁移任务还没有完成
            if (--i >= bound || finishing)
                advance = false;
            //没有元素需要迁移了  后续会去将扩容线程数减1,并判断扩容是否完成
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            //计算下一次任务迁移的开始桶位,并将这个值赋值给transferIndex
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        //如果没有更多的需要迁移的桶位
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            //扩容结束后,保存新数组,并重新计算扩容阈值,赋值给sizeCtl
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //扩容任务线程数减1
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                //判断当前所有扩容任务线程是否都执行完成
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                //所有扩容线程都执行完,标识结束
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        //当前迁移的桶位没有元素,直接在该位置添加一个fwd节点
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        //当前节点已经被迁移,当前线程不用再处理了
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        //当前桶位有数据,而且node节点 不是 fwd节点,说明这些数据需要迁移。
        else {
            //当前节点需要迁移,加锁迁移,保证多线程安全
            synchronized (f) {
                //f表示当前位置的那个元素
                if (tabAt(tab, i) == f) {
                    //ln 表示低位链表引用
                    //hn 表示高位链表引用
                    Node<K,V> ln, hn;
                    //表示当前桶位是链表桶位
                    if (fh >= 0) {
                        //fh表示f的hash值
                        //n表示原数组的长度,数组长度都是2的n次方,默认长度为16转为二进制为10000
                        //fh & n的结果就只有两种,要么是 0 要么是 n ,也就是说要么是 00000 ,要么是 10000
                        int runBit = fh & n;
                        //lastRun表示最后一个发生变化的节点。在这个节点之后的节点不需要动
                        Node<K,V> lastRun = f;
                        //找出最后一段完整的fh&n不变的链表,这样最后这一段链表就不用重新创建新结点了
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        //说明lastRun引用的链表为 低位链表,那么就让 ln 指向 低位链表
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        //说明lastRun引用的链表为 高位链表,就让 hn 指向 高位链表
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        //lastRun之前的结点因为fh&n不确定,所以全部需要重新迁移。
                        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);
                        //在原table中设置ForwardingNode节点以提示该桶扩容完成。
                        setTabAt(tab, i, fwd);
                        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;
                            }
                        }
                        //判断是否需要去树化
                        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;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

删除元素

remove()

public V remove(Object key) {
    return replaceNode(key, null, null);
}

final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        //删除时也需要确实扩容完成后才可以操作
        else if ((fh = f.hash) == MOVED)  
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            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;
                                //cv不为null则替换,否则是删除
                                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;
                        }
                    }
                    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));
					       }
                         }
                    }
                }
            }
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

ConcurrentHashmap基于JDK1.7

Segment数组的长度是不可以被改变的,初始化如果不规定,那么就采用默认的 2^4

ConcurrentHashMap中保存了一个默认长度为16的Segment[],每个Segment元素中保存了一个默认长度为2的HashEntry[],我们添加的元素,是存入对应的Segment中的HashEntry[]中。所以ConcurrentHashMap中默认元素的长度是32个,而不是16个

在这里插入图片描述

参数

ConcurrentHashMap 类

    // 默认初始容量是16, 和 HashMap 一样
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    // 默认加载因子是 0.75, 和 HashMap 一样
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    // 默认并发级别时16,这个并发级别决定了Segment数组的长度
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    // 每个Segment的table的最小容量
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    // 最多能有多少个segment
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    // 尝试对整个map进行操作(比如说统计map的元素数量),可能需要锁定全部segment;
    // 这个常量表示锁定所有segment前,尝试的次数
    static final int RETRIES_BEFORE_LOCK = 2;
    
    // 用于确定哪一个Segment的掩码值,hash的高几位用于选择Segment
    final int segmentMask;
    
    // 确定哪一个Segment的时候 hash偏移的位数
    final int segmentShift;
    
    // 最重要的属性,每个Segment可以看出一个Hash表,键值对是存在Segment中的
    final Segment<K,V>[] segments;

	......

Segment 类

    // 真正存储数据的数组,每个Segment独自有一个 table 存储键值对
    transient volatile HashEntry<K,V>[] table;
    
    // table 中元素的个数
    transient int count;
    
    // table 修改的次数
    transient int modCount;
    
    // 扩容阈值,它等于 capacity * loadFactor
    // capacity 就是数组 table 的长度
    transient int threshold;
    
    // 负载因子,每个Segment都相同
    final float loadFactor;

	......

Segment 内部自己维护一个Hash表,它有自己的扩容阈值和负载因子,负载因子每个Segment 都相同,之后扩容的时候是每个Segment自己扩容,不会影响到 ConcurrentHashmap 其他的 Segment。同时,可以看出Segment 是继承自 ReentrantLock 接口的,所以相当于每个Segment自己有一把锁,想要对Segment进行修改的时候需要先得到这个锁。

构造器

    //通过指定的容量,加载因子和并发等级创建一个新的ConcurrentHashMap
	@SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        //对容量,加载因子和并发等级做限制
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        //限制并发等级不可以大于最大等级
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        // 下面即通过并发等级来确定Segment的大小
        //sshift用来记录向左按位移动的次数
        int sshift = 0;
        //ssize用来记录Segment数组的大小
        int ssize = 1;
        //Segment的大小为大于等于concurrencyLevel的第一个2的n次方的数
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        //segmentMask的值等于ssize - 1(这个值很重要)
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //c记录每个Segment上要放置多少个元素
        int c = initialCapacity / ssize;
        //假如有余数,则Segment数量加1
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        //创建第一个Segment,并放入Segment[]数组中,作为第一个Segment
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
    }

    public ConcurrentHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
        putAll(m);
    }

添加元素

put()

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
    	//计算key的哈希值
        int hash = hash(key);
    	//因为一个键要计算两个数组的索引,为了避免冲突,这里取高位计算Segment[]的索引
        int j = (hash >>> segmentShift) & segmentMask;
    	//判断该索引位的Segment对象是否创建,没有就创建
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
    	//调用Segmetn的put方法实现元素添加
        return s.put(key, hash, value, false);
    }

ensureSegment()

//创建对应索引位的Segment对象,并返回
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;
    	// 注意,这里是getObjectVolatile这个方法,这个方法的意思就是,别的线程要是修改了segment[k],这个线程是可以看到操作后的结果
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            // 这里看到为什么之前要初始化 segment[0] 了,
            // segment[0] 就相当于一个初始化模板,
            // 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k],
            // 为什么要用“当前”,因为 segment[0] 可能早就扩容过了。
            
            // 获取当前 segment[0] 作为初始化模板
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            // 初始化 segment[k] 内部的哈希表。
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            // 再次检查一遍该槽是否被其他线程初始化了。
        	// 也就是在做上面那些操作时,看看是否有别的线程操作过 segment[k]。
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                // 构建新的 segment 对象。
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                // 再次检查 segment [k] 是否为null。
            	// 注意,这里是while,之前的if,也是起到如果下面操作失败,再次检查的作用。
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        // CAS操作,这里的比较并交换在CPU里面就是一条指令,保证原子性的。
                    	// 不存在那种比较完毕之后的间隙,突然切换到别的线程来修改这个值的情况。
                        break;
                }
            }
        }
        // 返回 segment 对象。
        // 这里返回的seg可能是自己new的,也可能是别的线程new的,反正只要其中一个就好了。
        return seg;
    }

Segment.put()

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    		// 在 put 到指定段中之前,我们得获取到当前段表中这个桶的独占的锁。
    		// 以此来保证整个过程,只有我们一个线程在对这个桶做操作。
            HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    		//用来存储被覆盖的值
            V oldValue;
            try {
                // 这个是段表某个桶内部的哈希表。
                HashEntry<K,V>[] tab = table;
                // 再次利用待插入键值对 key 的 hash 值,求应该放置的数组下标。
                int index = (tab.length - 1) & hash;
                // first 是桶中哈希表的待插入桶处的链表的表头。
                HashEntry<K,V> first = entryAt(tab, index);
                // 遍历链表
                for (HashEntry<K,V> e = first;;) {
                    // 如果不为空,就去判断是否存在该节点,存在就覆盖
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                         // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
                		// 如果不为 null
                        if (node != null)
                            //将它设置为头节点。JDK1.7这里使用的是头插法
                            node.setNext(first);
                        else
                            //不为空,就将它初始化再插入
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        // 如果超过了段表中该桶中哈希表的阈值,这个哈希表就需要扩容。
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            //扩容
                            rehash(node);
                        else
                            // 没有达到阈值,将 node 放到哈希表的 index 位置,
                    	    // 其实就是将新的节点设置成原链表的表头,使用头插法。
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                //释放锁
                unlock();
            }
            return oldValue;
        }

scanAndLockForPut()

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    	    // 简单来说就是拿到段表中某个桶中的哈希表数组中通过hash计算的那个下标下的第一个节点。
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
    		// 用于记录获取锁的次数。
            int retries = -1; // negative while locating node
    		// 循环获取锁。
    		// 如果获取失败,就会去进行一些准备工作。
            while (!tryLock()) {
                // 辅助变量用于重复检查,
        	    // 用来检查对应段表中那个桶上的哈希表数组中对应索引桶处,之前取出来的第一个节点是否还是我们之前取得那个。
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    // 判断段表中对应桶中的哈希表的对应桶上的节点 HashEntry 是不是还没被初始化。
                    if (e == null) {
                        if (node == null) // speculatively create node
                            // 进到这里说明数组该位置的链表是空的,没有任何元素。
                            // 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置。
                            // 将我们即将插进去的元素,构建成一个HashEntry节点对象。
                            node = new HashEntry<K,V>(hash, key, value, null);
                        // 将 retries 赋值为0,不让准备工作重复执行。
                        retries = 0;
                    }
                    // 否则的话,判断 key 是否有重复的
                    else if (key.equals(e.key))
                        // 将 retries 赋值为0,不让准备工作重复执行。
                        retries = 0;
                    else
                        // 否则顺着链表往下走
                        e = e.next;
                }
                // 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁,避免cpu空转。
        		// lock() 是阻塞方法,直到获取锁后返回。
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                 // 偶数次数才进行后面的判断。
                 // 这个时候出现问题了,那就是有新的元素进到了链表,成为了新的表头。
                 // 也可以说是链表的表头被其他线程改变了。
                 // 所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法。
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    // 此时怎么做呢,
            	    // 别的线程修改了该segment的节点,重新赋值e和first为最初值,和第一二行代码一样的效果。
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
    		// 将准备工作制作好的节点返回。
            return node;
        }

rehash()

	// 传入的参数 node 是这次扩容后,需要添加到新的数组中的数据。
	private void rehash(HashEntry<K,V> node) {
        	// 用来存储待扩容槽中的哈希表旧表
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
        	// 新的容量为旧容量的 2 倍
            int newCapacity = oldCapacity << 1;
        	// 设置新的扩容阈值
            threshold = (int)(newCapacity * loadFactor);
        	// 用上面的参数创建新的哈希表
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
        	// 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 000...00011111
            int sizeMask = newCapacity - 1;
        	// 遍历原数组,将原哈希表索引 i 处的链表拆分到新哈希表索引 i 和 i+oldCap 两个位置,高位和低位。
            for (int i = 0; i < oldCapacity ; i++) {
                // e 是链表的第一个元素。
                HashEntry<K,V> e = oldTable[i];
                // 如果这条链不是空链的话
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                     // 计算当前节点应该放置在新数组中的位置,
            		// 假设原哈希表长度为 16,e 在 oldTable[3] 处,那么 e 在新哈希表中的索引 idx 只可能是 3 或者是 3 + 16 = 19。
                    int idx = e.hash & sizeMask;
                    // 该位置处只有一个元素
                    if (next == null)   //  Single node on list
                        // 直接将该节点设置到这里
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        // e 是链表表头
                        HashEntry<K,V> lastRun = e;
                        // idx 是当前链表的头结点 e 的新位置
                        int lastIdx = idx;
                        // 下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的所有元素是将要放到一起的。
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                         // 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        // 下面的操作是处理 lastRun 之前的节点,
                	    // 这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
        	// 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

删除元素

remove()

 public V remove(Object key) {
     	// 得到待删除的key的哈希值
        int hash = hash(key);
     	// 根据hash确定segment
        Segment<K,V> s = segmentForHash(hash);
     	// 调用segment.remove进行删除
        return s == null ? null : s.remove(key, hash, null);
    }
 final V remove(Object key, int hash, Object value) {
     		// 获取锁失败,则不断自旋尝试获取锁
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table;
                // 定位到segment中table的哪个位置
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                HashEntry<K,V> pred = null;
                // 遍历链表
                while (e != null) {
                    K k;
                    HashEntry<K,V> next = e.next;
                    // 如果key和hash都匹配
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        // 如果没有传入value,则直接删除该节点
                        // 如果传入了value,比如调用的map.remove(key,value),则要value匹配才会删除,否则不操作
                        if (value == null || value == v || value.equals(v)) {
                            // 头结点就是要找删除的元素,next为null,则将null赋值数组的该位置
                            if (pred == null)
                                setEntryAt(tab, index, next);
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    // 不匹配时,pred保存当前一次检测的节点,e指向下一个节点
                    pred = e;
                    e = next;
                }
            } finally {
                // 释放锁
                unlock();
            }
            return oldValue;
        }

总结

JDK7中的ConcurrentHashMap是怎么保证并发安全

主要利用Unsafe操作+ReentrantLock+分段思想。
主要使用了Unsafe操作中的:
1. compareAndSwapObject:通过cas的方式修改对象的属性
2. putOrderedObject:并发安全的给数组的某个位置赋值
3. getObjectVolatile:并发安全的获取数组某个位置的元素

分段思想是为了提高ConcurrentHashMap的并发量,分段数越高则支持的最大并发量越高,程序员可以通过concurrencyLevel参数来指定并发量。ConcurrentHashMap的内部类Segment就是用来表示某一个段的。

每个Segment就是一个小型的HashMap的,当调用ConcurrentHashMap的put方法是,最终会调用到Segment的put方法,而Segment类继承了ReentrantLock,所以Segment自带可重入锁,当调用到Segment的put方法时,会先利用可重入锁加锁,加锁成功后再将待插入的key,value插入到小型HashMap中,插入完成后解锁。

JDK7中的ConcurrentHashMap的底层原理

ConcurrentHashMap底层是由两层嵌套数组来实现的:
1.ConcurrentHashMap对象中有一个属性segments,类型为Segment[];
2.Segment对象中有一个属性table,类型为HashEntry[];

当调用ConcurrentHashMap的put方法时,先根据key计算出对应的Segment[]的数组下标j,确定好当前key,value应该插入到哪个Segment对象中,如果segments[j]为空,则利用自旋锁的方式在j位置生成一个Segment对象。

然后调用Segment对象的put方法。

Segment对象的put方法会先加锁,然后也根据key计算出对应的HashEntry[]的数组下标i,然后将key,value封装为HashEntry对象放入该位置,此过程和JDK7的HashMap的put方法一样,然后解锁。

在加锁的过程中逻辑比较复杂,先通过自旋加锁,如果超过一定次数就会直接阻塞等等加锁。

JDK8中的ConcurrentHashMap是怎么保证并发安全

主要利用Unsafe操作+synchronized关键字。
Unsafe操作的使用仍然和JDK7中的类似,主要负责并发安全的修改对象的属性或数组某个位置的值。

synchronized主要负责在需要操作某个位置时进行加锁(该位置不为空),比如向某个位置的链表进行插入结点,向某个位置的红黑树插入结点。

JDK8中其实仍然有分段锁的思想,只不过JDK7中段数是可以控制的,而JDK8中是数组的每一个位置都有一把锁。

当向ConcurrentHashMap中put—个key,value时,
1.首先根据key计算对应的数组下标i,如果该位置没有元素,则通过自旋的方法去向该位置赋值。
2.如果该位置有元素,则synchrpnized会加锁
3.加锁成功之后,在判断该元素的类型
    a.如果是链表节点则进行添加节点到链表中
    b.如果是红黑树则添加节点到红黑树
4.添加成功后,判断是否需要进行树化
5.addCount,这个方法的意思是ConcurrentHashMap的元素个数加1,但是这个操作也是需要并发安全的,并且元素个数加1成功后,会继续判断是否要进行扩容,如果需要,则会进行扩容,所以这个方法很重要。
6.同时一个线程在put时如果发现当前ConcurrentHashMap正在进行扩容则会去帮助扩容。

ConcurrentHashMap 1.7 和1.8的区别

实现上的区别:
JDK1.7使用的 Segment + HashEntry + Unsafe
JDK1.8使用的 Synchronized + CAS + Node + Unsafe

put()方法的区别:
JDK1.7
1、需要定位 2 次 (segments[i],segment中的table[i]),也就是两次hash
2、没获取到 segment 锁的线程,没有权力进行put操作,而是会去做下一个put操作的准备工作

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

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

JDK1.8
jdk1.8的扩容支持并发迁移节点,从old数组的尾部开始,如果该桶被其他线程处理过了,就创建一个 ForwardingNode 放到该桶的首节点,hash值为-1,其他线程判断hash值为-1后就知道该桶被处理过了。

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

JDK1.8
用一个 baseCount 变量来记录 ConcurrentHashMap 当前 节点的个数。
先尝试通过CAS 修改 baseCount 
如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将 CELLSBUSY 置1,成功则可以把 baseCount变化的次数 暂存到一个数组 counterCells 里,后续数组 counterCells 的值会加到 baseCount 中。
如果 CELLSBUSY 置1失败又会反复进行CAS baseCount 和 CAScounterCells 数组
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMap是Java中的一个并发容器,用于在多线程环境中安全地存储和访问键值对。它使用了一些特殊的技术来提高其并发性能。 ConcurrentHashMap分析可以从几个关键点开始。首先,它使用了大量的CAS(Compare and Swap)操作来代替传统的重量级锁操作,从而提高了并发性能。只有在节点实际变动的过程中才会进行加锁操作,这样可以减少对整个容器的锁竞争。 其次,ConcurrentHashMap的数据结构是由多个Segment组成的,每个Segment又包含多个HashEntry。这样的设计使得在多线程环境下,不同的线程可以同时对不同的Segment进行操作,从而提高了并发性能。每个Segment都相当于一个独立的HashMap,有自己的锁来保证线程安全。 在JDK1.7版本中,ConcurrentHashMap采用了分段锁的设计,即每个Segment都有自己的锁。这样的设计可以在多线程环境下提供更好的并发性能,因为不同的线程可以同时对不同的Segment进行读写操作,从而减少了锁竞争。 总的来说,ConcurrentHashMap通过使用CAS操作、分段锁以及特定的数据结构来实现线程安全的并发访问。这使得它成为在多线程环境中高效地存储和访问键值对的选择。123 #### 引用[.reference_title] - *1* [ConcurrentHashMap 码解析](https://blog.csdn.net/Vampirelzl/article/details/126548972)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* *3* [ConcurrentHashMap分析](https://blog.csdn.net/java123456111/article/details/124883950)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值