ConcurrentHashMap探究

ConcurrentHashMap

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

  • 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是否变化来确定的。
ConcurrentHashMap在JDK8中的实现
阅读 671

推荐阅读
刨刨代码的根
用户专栏

刨根问底拦不住~

0 人关注
8 篇文章
专栏主页