ConcurrentHashMap 原理分析

hashMap 在 1.7 和 1.8 中的区别

1、 引入了红黑树
2、解决了并发环境下,死链的情况(在1.7的rehash中,会进行链表的反转插入,会引发死链问题,java8 中不会,8中是采用修改头部节点的位置来实现)
3、1.7 扩容发送在,插入数据之前,会先检查,扩容后插入。 1.8是在插入数据之后,在判断是否需要扩容。
4、为什么1.8解决了死链问题还是不安全,在多线程的情况下,可能会put遗漏数据。

ConcurrentHashMap

hashmap固然非常优秀,但是在高并发的情况下,他存在着一些不足的情况。所以这里来分析一下ConcurrentHashMap的原理

sizeCtl

英语好,胜过一切


    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

含义有如下3中情况:

  1. -1 表示正在有线程进行初始化
  2. '>0 的情况,标识map的负载容量
  3. 负数(非 -1) 代表有几个线程正在扩容,例如 -2 有一个线程在扩容

ConcurrentHashMap

cas +sync 的方式来代替segment

1、并发扩容
2、通过数组的方式来统计并发情况下的元素增加

addCount();

分片方式
base + 采用了数组累加的方式,
用ThreadLocalRandom 来获取,随机数,来确定数组的位置,
解决并发问题引起的数值大小问题。例如 LongAddr 采用的是同一个思想

putvalue

增加了sync ,锁住了头结点,相比于1.7的分段锁,锁的粒度更细了。

扩容

chm的扩容是可以多个线程并行来扩容的。

transfer

1、扩大数据
2、转移数据链

分片思维,设置分工区间。

把链表进行分类 n为当前链表长度 :
a hash & n = 0
b hash & n != 0

ConcurrentHashMap
阶段一

HashMap 是开发中高频集合。可看另一篇文章####
怎么去解决它的并发问题:
1、HashTable
2、Collections.synchronizedMap(hashMap)
3、ConcurrentHashMap

1、2 方法类似,都是对对象增加一个锁,由于锁粒度粗,所以效率并不高。

阶段二
1.7的 ConcurrentHashMap

在1.7中,ConcurrentHashMap 采用分段式锁来控制并发。
Segment 继承ReentrantLock充当锁的角色,每个Segment,负责一部分的桶。比如一个Segment 负责8个桶,那么当有值落在这几个桶内时,需要先获取Segment对象,否则需要先等待。

相比于HashTable的全局锁,Segment的粒度更细,所以在并发环境下,性能更好。

1.8的 ConcurrentHashMap

在1.7把锁细化后,可能是觉得效果不错,于是在1.8中,通过cas和Synchronized来更加细化锁的粒度,每个桶使用一个锁。同时引入了红黑树的结构。
至于为什么使用Synchronized而不是lock可能是sync又进行了一波升级。

Synchronized 的重要改变是在1.6后区分了偏向锁、轻量级锁、重量级锁。提升了整个java内置锁的效率。

阶段三 源码分析(1.8)
重要参数
  • Node<K,V>[] table : 当前的node数组,也就是桶数组。默认为null
  • Node<K,V>[] nextTable:默认为null,扩容时新生成的数组
  • sizeCtl:是这个ConcurrentHashMap中一个很关键的属性,先看下源码中的注释:

Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.

中文可以翻译如下几个点:

  1. 当为-1时,容器正在初始化
  2. 0 时,代表容器的容量

  3. 当为 非-1 的负数时,代表有几个线程正在扩容。如 -2 时,代表一个线程在扩容
    4.当table未初始化的时候,表示初始化后的大小。
  • Node 单位数据结构如下
class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ... 
}
  • ForwardingNode 一个特殊的Node节点,hash值为-1,只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动
map 长度

由于 这个类的get和put很复杂,但是所使用的思想是一致的,所以这里先分析一下size的维护。
总体思想:size的维护采用LongAddr的方法,使用一个base+数组的方式来统计最后的长度。当并发情况下,采用数组中不同的桶,来记录数量的变化,最后再累加起来。

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
       // 当数组不为null时,采用累加的方式统计
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

同时,在添加个数时,如下:

// x 为添加的个数,通常为1
// check  分2种情况,
//当 check< 0 ,不检查是否在扩容情况下  
// 当 check <= 1 ,只检查是否存在竞争,多个线程在插入数据
private final void addCount(long x, int check) {
        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();
        }
        
        // 分界线 以下情况,说明在添加个数时,整个map正在扩容
        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();
            }
        }
    }

上面代码比较多,可以总结如下2个功能:
1、增加元素个数
2、判断元素是否需要扩容
第一个if里面的。主要做了如下的事,在数组中选择一个下标用来存储,然后判断改下标是否存在竞争,然后具体操作交给fullAddCount 方法来。分析如下:

1、counterCells != null 初始化完成
2、通过cas对basecount增加
3、获得随机下标
4、判断这个下标的竞争状态
5、调用fullAddCount 方法来完成计数

fullAddCount 方法如下:

private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        // --- 判断是否是随机,不是随机的话,随机取一个值
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        
        // 自旋操作
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            // 1 进入时counterCells已经被初始化后,走下面的逻辑
             if ((as = counterCells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {            // Try to attach new Cell
                        CounterCell r = new CounterCell(x); // Optimistic create
                        if (cellsBusy == 0 &&
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// 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
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
            
            // 2 当counterCells 还没有被初始化时,走入下链路
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 3 通过cas basecount就添加成功
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

为了清晰一点,单独先拿2出来代码比较:

// 
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2];
                        rs[h & 1] = new CounterCell(x);
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            

代码的功能流程如下
1、检查是否是第一次进入 cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)
2、初始化CounterCell 数组 CounterCell[] rs = new CounterCell[2];
3、在随机的下标中,添加rs[h & 1] = new CounterCell(x);
构造函数

//使用了sun.misc.Contended 来防止伪共享的存在
@sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

此时,再回过头来看1中的代码,就是在已经有数组的情况下增加计数,不做累述。

阶段四、个人愚见

到目前为止,虽然只分析了map中,size的维护方式,采用了数组分片的方式来处理并发。但是可以想见,map在后续的方法中也会采用这种思想来完成高并发下的安全。也许可以在日后的日常开发中运用起来这种方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值