ConcurrentHashMap探究

ConcurrentHashMap

ConcurrentHashMap是线程安全,性能出色的Map的线程安全实现,相比较HashMap他是线程安全的,相比较HashTable他的性能优点很是明显。他的使用很简单,这里主要是想要探究一下ConcurrentHashMap的实现原理。
在这里一共有 个问题须要搞明白。java

  • ConcurrentHashMap为何比HashTable的性能要高?
  • ConcurrentHashMap在JDK8和JDK7有什么变化,为何会有这种变化,对咱们开发有什么启示?
  • 为何在JDK8中使用Synchronized而不是用ReentrantLock来实现加锁?

带着这几个问题咱们来分析一下ConcurrentHashMap的源码吧。算法

ConcurrentHashMap定义

在JDK8(JDK7也是同样)中ConcurrentHashMap的定义以下:数组

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
ConcurrentHashMap在JDK7中的实现

Java7中ConcurrentHashMap的实现是基于分段锁实现的。他的底层数据结构仍然是数组+链表,与HashTable不一样的是,ConcurrentHashMap的最外层不是一个大的数组,而是一个Segment数组(分段锁的实现)。分段锁减少了锁的粒度,提升了并发程度。这也是为何比HashTable效率要高的缘由。
HashTable的源码其实很简单,HashTable和HashMap的结构一致,可是每个方法都是用Synchronized来修饰,以保证操做是线程安全的。这样在多线程的状况下,只有一个线程获取锁操做hashTable中的数据。而CourrentHashMap则不是,它容许最多有segment数组长度个线程同时操做ConcurrentHashMap中的数据。安全

ConcurrentHashMap的总体结构以下(图片来源:http://www.jasongj.com/java/c...):
图片描述数据结构

ConcurrentHashMap的定义:多线程

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;

    /**
     * 表的默认容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 默认扩容因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * segments数组的默认长度,为了能经过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * HashEntry最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * segment的最小容量
     */
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    /**
     * segment的最大容量
     */
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    /**
     * 重试次数,无锁的状况下尝试两次
     */
    static final int RETRIES_BEFORE_LOCK = 2;

    /**
     * 散列运算的掩码,等于ssize-1
     */
    final int segmentMask;

    /**
     * 定位参与散列运算的位数,等于32-sshift
     */
    final int segmentShift;

    /**
     * 定义segment数组
     */
    final Segment<K,V>[] segments;

Segment定义:并发

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    transient int modCount;
    //扩容量,默认为表的容量*加载因子,实际量超过这个值的时候,进行扩容
    transient int threshold;
    //segment中hashEntry的扩容因子
    final float loadFactor;

}
  • get()操做通过两次散列找到HashEntry,而后进行遍历的操做,get方法使用的变量都是volatile类型的,能够保证线程可见,可以被多线程同时读,而且保证不会读取到过时的值,在get操做中须要读count和value两个共享变量,因此不须要加锁,volatile字段的写入操做会先于读操做,全部即便有一个线程在修改,get也能获取到最新的值。
  • put() 先对变量的hashCode进行一次再散列而后定位到Segment,而后再Segment中进行插入操做。若是HashEntry数组超过threshold,那么扩容,扩容只是针对Segment进行扩容。
  • size() 统计ConcurrentHashMap中元素的个数,须要对Segment中全部元素进行求和,Segment里全局变量count是一个volatile类型的变量,累加能够获取元素的总个数,可是不必定准确,由于使用过的count再后面能够改变,最后的方法就是阻塞put,remove,clean等元素操做的方法,可是这样很是低效。因此Concurrenthashmap经过尝试两次不锁来统计segment的元素大小,若是两次结果不同,那么使用加锁的方式来统计,容器是否变化是经过modCount是否变化来肯定的。
相关文章
相关标签/搜索