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

在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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值