如何在很短的时间内将大量数据插入到ConcurrentHashMap(转)

将大批量数据保存到map中有两个地方的消耗将会是比较大的:第一个是扩容操作,第二个是锁资源的争夺。第一个扩容的问题,主要还是要通过配置合理的容量大小和扩容因子,尽可能减少扩容事件的发生;第二个锁资源的争夺,在put方法中会使用synchonized对头节点进行加锁,而锁本身也是分等级的,因此我们的主要思路就是尽可能的避免锁等级。所以,针对第二点,我们可以将数据通过通过ConcurrentHashMap的spread方法进行预处理,这样我们可以将存在hash冲突的数据放在一个组里面,每个组都使用单线程进行put操作,这样的话可以保证锁仅停留在偏向锁这个级别,不会升级,从而提升效率。

其他:

ConcurrentHashMap 1.7和1.8的区别
1、整体结构
1.7:Segment + HashEntry + Unsafe

1.8: 移除Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe

2、put()
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

3、get()
基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。

4、resize()
1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。

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

5、size()
1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。

1.8:用baseCount来存储当前的节点个数,这就设计到baseCount并发环境下修改的问题(说实话我没看懂-_-!)。

JDK1.7中扩容中转移数组transfer()方法源码。

void transfer(Entry[] newTable, boolean rehash) {
    //新table的容量
    int newCapacity = newTable.length;
    //遍历原table
    for (Entry<K,V> e : table) {
        while(null != e) {
            //保存下一次循环的 Entry<K,V>
            Entry<K,V> next = e.next;
            if (rehash) {
                //通过e的key值计算e的hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //得到e在新table中的插入位置
            int i = indexFor(e.hash, newCapacity);
            //采用链头插入法将e插入i位置,最后得到的链表相对于原table正好是头尾相反的
            e.next = newTable[i];
            newTable[i] = e;
            //下一次循环
            e = next;
        }
    }
}


原文链接:https://blog.csdn.net/u013374645/article/details/88700927

猜你喜欢

转载自www.cnblogs.com/Esquecer/p/11418350.html