Java-Concurrent框架--ConcurrentHashMap源码解析(JDK1.7)

来源:互联网 发布:阿里云备案域名介入 编辑:程序博客网 时间:2024/06/06 00:49

在Map的实现类中除了HashMap和HashTable以外还有一个ConcurrentHashMap。HashMap和HashTable一都非常熟悉,HashTable是线程安全的、且不能存储Null值,HashMap是非线程安全的、可以存储Null值。

ConcurrentHashMap是java.util.concurrent包中的类,他是结合HashMap和HashTable而来的Map实现类。从名字就知道是一种线程安全的Map类,那他和线程安全的HashTable有什么区别,往下看就知道了。

一 内部结构

在介绍ConcurrentHashMap内部结构之前,先复习下HashTable,就知道ConcurrentHashMap内部结构的设计原理了

1.1 HashTable

先看一下HashTable主要方法的方法定义:

public synchronized int size()public synchronized boolean isEmpty()public synchronized V get(Object key)public synchronized V put(K key, V value)

通过上面的方法定义,可以知道HashTable之所以能够保证线程安全,是因为基本上HashTable所有的方法都通过synchronized进行了锁保护,而且是将整个HashTable对象加锁,也就是同时只能有一个线程访问、修改HashTable。这样带来的问题就是在多线程环境中代价大、效率低。

1.2 ConcurrentHashMap

HashTable效率低速度慢的原因就在于每次都要将整个HashTable锁起来,ConcurrentHashMap正是巧妙的解决了这个问题,每次只将要被修改的局部地方加上锁,其他地方还可以访问和修改。

ConcurrentHashMap的内部结构通过两个内部类实现:Segment类和HashEntry类。

每个ConcurrentHashMap对象所有数据都是存在Segment< K,V>[] segments数组里,数组的每个元素都是一个Segment对象;Segment类的主要数据结构是HashEntry< K,V>[] table数组,table数组中的HashEntry是一个链表,链表的每个结点存储一个键值对。ConcurrentHashMap中每一个键值对最终就是存储在这里。

所以说ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点)。

然后每次只对segments数组中的一个Segment对象加锁,这样segments数组中跟其他Segment对象就可以被正常访问了。所以说ConcurrentHashMap是HashMap和HashTable的结合。

ConcurrentHashMap就是通过这种局部加锁的方式提高并发访问的速度。下图就是两种Map实现类的结构对比。
这里写图片描述

二 类关系图

ConcurrentHashMap类位于java.util.concurrent包当中,实现了ConcurrentMap接口,继承了AbstractMap。

这里写图片描述

然后我们再看看ConcurrentHashMap内部类Segment的类关系图,可以发现Segment继承了Reentrantlock类,正是他完成了多线程的同步控制
这里写图片描述

三 ConcurrentHashMap类定义

我们从主要成员变量、构造函数、常用API三个方面了解ConcurrentHashMap的定义

3.1 主要成员变量

上面第一节讲了ConcurrentHashMap有两个重要的内部类Segment类和HashEntry类,下面我们就看看他们的源码

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>        implements ConcurrentMap<K, V>, Serializable {    final Segment<K,V>[] segments;    static final class Segment<K,V> extends ReentrantLock implements Serializable {        transient int count;  //用于记录每个Segment桶中键值对的个数        transient int modCount;  //对table的大小造成影响的操作的数量(比如put或者remove操作)        transient int threshold;  //阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容        transient volatile HashEntry<K,V>[] table;  //链表数组,数组中的每一个元素代表了一个链表的头部        final float loadFactor;  //负载因子,用于确定threshold,默认是1    }    static final class HashEntry<K,V> {        final int hash;        final K key;        volatile V value;        volatile HashEntry<K,V> next;    }}

可以看出来Segment是个锁对象,HashEntry是链表的一个结点,HashEntry.Key存的是每个键值对的键值,HashEntry.Value存的键值对的值。而且大多数变量都是final修饰的。

3.2 构造函数

ConcurrentHashMap一共有五个构造函数,我们重点分析下面第一个就行,其他三个都是这个构造函数实现的。最后一个构造函数相当于是复制一个新的对象。

  • public ConcurrentHashMap(int ,float , int)
  • public ConcurrentHashMap(int ,float)
  • public ConcurrentHashMap(int)
  • public ConcurrentHashMap()
  • public ConcurrentHashMap(Map
public ConcurrentHashMap(int initialCapacity,                             float loadFactor, int concurrencyLevel) {        // initialCapacity是Map中键值对初始个数        // loadFactor是负载参数        // concurrentLevel代表ConcurrentHashMap内部的Segment的数量        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        int sshift = 0;           int ssize = 1;  // Segment[]数组大小        while (ssize < concurrencyLevel) {            ++sshift;            ssize <<= 1;        }        this.segmentShift = 32 - sshift;        this.segmentMask = ssize - 1;        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        int c = initialCapacity / ssize;        if (c * ssize < initialCapacity)            ++c;        int cap = MIN_SEGMENT_TABLE_CAPACITY;        while (cap < c)            cap <<= 1; // table[]数组大小        // create segments and segments[0]        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);        this.segments = ss;    }

在初始化过程中涉及几个关键的量,我们先看构造函数的三个参数:

  • initialCapacity:ConcurrentHashMap中键值对总个数,如果不指定是默认取16
  • loadFactor:负载参数,用于确定table[]数组扩容阈值,就是当table[ ]数组长度大于(cap * loadFactor)时,进行扩容,未指定是默认取0.75
  • concurrencyLevel:代表ConcurrentHashMap内部的segment[ ]数组的长度,concurrencyLevel在初始化后是不可改变的,也就是说segment[ ]数组长度是不可变得,所以扩容操作主要针对于table[ ]数组。这样的好处在于扩容是不需要对整个ConcurrentHashMap做rehash,只对某个table[ ]数组做rehash即可。未指定是默认取16

构造函数主要功能就是创建了sagment[ ]数组和table[ ]数组,而重点就在于计算这两个数组的大小。

  • ssize:sagment[ ]数组的真实大小,sszie是不大于concurrencyLevel的最大的2的指数,他的好处在于方便采用移位操作进行hash,加快速度
  • cap:table[ ]数组的大小,cap是不大于initialCapacity / ssize的最大的2的指数,好处同样也是加快hash

另外还有两个重要的变量segmentShift和segmentMask:

  • segmentShift
  • segmentMask

这两个量是用来定位segment桶在segments[]数组中位置用的,详见“4.1定位sagment桶”

3.3 常用API

  • public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) 构造函数
  • public boolean isEmpty( )
  • public int size()
  • public V get(Object key)
  • public boolean containsKey(Object key)
  • public boolean containsValue(Object value)
  • public V put(K key, V value)
  • public V remove(Object key)
  • public void clear()
  • public Set< K> keySet()

四 常用方法源码分析

4.1 定位sagment桶

在ConcurrentHashMap的增删改查操作中有一个步骤至关重要,就是要先通过key值确定键值对究竟是存在segments[ ]数组中哪个位置,table[ ]数组中哪个位置。

    private Segment<K,V> segmentForHash(int h) {        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);    }

参数h就是hash值,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,这里的segmentShift和segmentMask值是在构造函数中计算得到的,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。

这里有两个地方我一直还没搞懂,一是“>>> segmentShift”操作和“& segmentMask”感觉是重复的,不知道为什么要这样,多一次计算确保正确?二是后面的“<< SSHIFT) + SBASE”一直还没搞明白是在干啥。。。

计算table[]数组中的位置通过int index = (table.length - 1) & hash;实现。

4.2 put()

    // ConcurrentHashMap类的put()方法    public V put(K key, V value) {        Segment<K,V> s;        if (value == null)            throw new NullPointerException();        int hash = hash(key);        int j = (hash >>> segmentShift) & segmentMask;        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment            s = ensureSegment(j);        // 调用Segment类的put方法        return s.put(key, hash, value, false);      }    // Segment类的put()方法    final V put(K key, int hash, V value, boolean onlyIfAbsent) {            // 加锁            HashEntry<K,V> node = tryLock() ? null :                scanAndLockForPut(key, hash, value);            V oldValue;            try {                HashEntry<K,V>[] tab = table;                // 根据hash计算在table[]数组中的位置                int index = (tab.length - 1) & hash;                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 {                        if (node != null)                            node.setNext(first);                        else                            node = new HashEntry<K,V>(hash, key, value, first);                        int c = count + 1;                        // 判断table[]是否需要扩容,并通过rehash()函数完成扩容                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)                            rehash(node);                        else                            setEntryAt(tab, index, node);                        ++modCount;                        count = c;                        oldValue = null;                        break;                    }                }            } finally {                unlock();            }            return oldValue;        }

可以发现put的基本过程就是,通过hash值确定segments[]中segment桶的位置,然后调用Segment的put方法将键值对插入segment桶的table[]数组中,先确定table[]数组是否存在该key值和对应的位置,再插入到具体的链表位置。

有以下几点值得关注:

  • 同步锁:多线程同步锁是在调用Segment类的put方式时使用的,此时其他线程不能访问当前segment桶,但是可以其他segment桶
  • 扩容:在添加新的键值对之前会判断当前segment桶大小是否超过阈值threshold ,如果超过就调用rehash()进行扩容,newCapacity = oldCapacity << 1;将table[]大小扩容到原来的两倍

4.3 get( )

    public V get(Object key) {        Segment<K,V> s; // manually integrate access methods to reduce overhead        HashEntry<K,V>[] tab;        int h = hash(key);        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&            (tab = s.table) != null) {            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);                 e != null; e = e.next) {                K k;                if ((k = e.key) == key || (e.hash == h && key.equals(k)))                    return e.value;            }        }        return null;    }

get( )方法的实现过程比较简单,首先计算hash值并确定segment桶,然后确定table[]数组中HashEntry的位置,遍历HashEntry链表,获取Value值

4.4 remove( )

    // ConcurrentHashMap类的remove方法    public V remove(Object key) {        int hash = hash(key);        // 获取segment桶        Segment<K,V> s = segmentForHash(hash);          // 调用Segment类的remove方法        return s == null ? null : s.remove(key, hash, null);    }    Segment类的remove方法    final V remove(Object key, int hash, Object value) {            // 进行加锁            if (!tryLock())                scanAndLock(key, hash);            V oldValue = null;            try {                HashEntry<K,V>[] tab = 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;                    if ((k = e.key) == key ||                        (e.hash == hash && key.equals(k))) {                        V v = e.value;                        if (value == null || value == v || value.equals(v)) {                            if (pred == null)                                setEntryAt(tab, index, next);                            else                                pred.setNext(next);                            ++modCount;                            --count;                            oldValue = v;                        }                        break;                    }                    pred = e;                    e = next;                }            } finally {                unlock();            }            return oldValue;        }

理解了put( )和get( )操作后,删除操作也就自然理解了。

之前有的版本的JDK中,HashEntry类的实现中next的声明为:final HashEntry

4.5 size( )

public int size() {        // Try a few times to get accurate count. On failure due to        // continuous async changes in table, resort to locking.        final Segment<K,V>[] segments = this.segments;        int size;        boolean overflow; // true if size overflows 32 bits        long sum;         // sum of modCounts        long last = 0L;   // previous sum        int retries = -1; // first iteration isn't retry        try {            for (;;) {                if (retries++ == RETRIES_BEFORE_LOCK) {                    // 给所有的segment桶加锁                    for (int j = 0; j < segments.length; ++j)                        ensureSegment(j).lock(); // force creation                }                sum = 0L;                size = 0;                overflow = false;                for (int j = 0; j < segments.length; ++j) {                    Segment<K,V> seg = segmentAt(segments, j);                    if (seg != null) {                        sum += seg.modCount;                        int c = seg.count;                        if (c < 0 || (size += c) < 0)                            overflow = true;                    }                }                if (sum == last)                    break;                last = sum;            }        } finally {            if (retries > RETRIES_BEFORE_LOCK) {                for (int j = 0; j < segments.length; ++j)                    segmentAt(segments, j).unlock();            }        }        return overflow ? Integer.MAX_VALUE : size;    }

可以发现size( )方法很不一样:

  • size( )方法会将整个ConcurrentHashMap遍历一次,来获得map大小
  • 遍历之前会将ConcurrentHashMap中每个segment桶都加锁

五 总结

ConcurrentHashMap是结合HashTable和HashMap得到的线程安全的保证高并发的Map实现类

  • ConcurrentHashMap主要结构是一个Segment[]数组,每个Segment元素都有一个HashEntry[]数组,Segment相当于一个小的HashTable
  • Segment[]数组大小是不可变的,扩容操作是将Segment类中HashEntry[]扩容到原来的两倍
  • put( )和remove( )方法是要加锁的,而且只对单个segment桶加锁
  • size( )需要遍历整个ConcurrentHashMap,而且每个segment桶都要加锁

参考资料:
http://www.iteye.com/topic/1103980
http://www.cnblogs.com/ITtangtang/p/3948786.html

0 0
原创粉丝点击