美文网首页技术干货Android技术知识Android知识
Java之Hashtalbe与Currenthashmap实现原

Java之Hashtalbe与Currenthashmap实现原

作者: dotaer_shashen | 来源:发表于2017-03-15 15:10 被阅读0次
    Hashtalbe与Hashmap的数据存储方式是一样的 线程安全的hashmap synchronized;
    Hashtable 与 HashMap 的简单比较
    1. HashTable基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是可将任何键映射到相应值的类的抽象父类(每个键和值都是对象,Dictionary 这个类过时了,新的实现类应该实现Map接口),而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作;

    2. HashMap的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException;

    3. Hashtable方法是同步(线程安全),而HashMap(线程不安全)则不是。源码中,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,有些方法也是在内部通过 synchronized 代码块来实现;

    如果在jdk5以上需要使用线程安全的HashMap,可以使用HashTable的升级Currenthashmap;
    也可以加入下面的代码进行同步:

    Map m = Collections.synchronizeMap(hashMap);
    
    Currenthashmap实现原理

    分段锁:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个数据段时,其他段的数据也能被其他线程访问;
    并发度:并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度(默认为16);

    CurrentHashMap的数据储存结构如下图所示

    ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类HashMap的结构,Segment内部维护了一个链表数组;从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高;

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        transient volatile int count;
        transient int modCount;
        transient int threshold;
        transient volatile HashEntry<K,V>[] table;
        final float loadFactor;
    }
    

    详细解释一下Segment里面的成员变量的意义:

    • count:Segment中元素的数量
    • modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
    • threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
    • table:链表数组,数组中的每一个元素代表了一个链表的头部
    • loadFactor:负载因子,用于确定threshold

    Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构;

    static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;
    }
    

    可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况;

    CurrentHashMap的构造函数
    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
        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;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        segmentShift = 32 - sshift;
        segmentMask = ssize - 1;
        this.segments = Segment.newArray(ssize);
      
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;
      
        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);
    }
    

    CurrentHashMap的初始化一共有三个参数,一个initialCapacity,表示初始的容量;一个loadFactor,表示负载参数;最后一个是concurrentLevel,代表ConcurrentHashMap内部的Segment的数量;

    ConcurrentHashMap的初始化:
    1.先是根据concurrentLevel来new出Segment,这里Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash,加快hash的过程;(concurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致需要扩容,不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了;)

    2.接下来就是根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数,同样使为了加快hash的过程;(可以理解为一个HashMap数组,每个数组里面有一个hashmap,那么容量大小实际上是等于intialCapacity*负载因子);

    3.变量segmentShift和segmentMask,这两个变量在后面将会起到很大的作用,假设构造函数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减1;

    ConcurrentHashMap是如何确定segment的位置的:
    我们看一下ConcurrentHashMap的get方法;

    public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
    }
    

    这里的get操作是没有加锁的,看第三行用来确定segment的位置

    final Segment<K,V> segmentFor(int hash) {
        return segments[(hash >>> segmentShift) & segmentMask];//定位Segment所使用的hash算法
    }
    

    segmentFor函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中,在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法;

    V get(Object key, int hash) {
        if (count != 0) { // read-volatile
            HashEntry<K,V> e = getFirst(hash);
            while (e != null) {
                if (e.hash == hash && key.equals(e.key)) {
                    V v = e.value;
                    if (v != null)
                        return v;
                    return readValueUnderLock(e); // recheck
                }
                e = e.next;
            }
        }
        return null;
    }
    

    可以看到第一次算出的hash值被当被参数传进来了,用来确定,segment里面数组的下标;
    再来看第二行代码,这里对count进行了一次判断,其中count表示Segment中元素的数量,我们可以来看一下count的定义:

    transient volatile int count;
    

    因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容;所以get方法没有加锁;
    然后,在第三行,调用了getFirst()来取得链表的头部:

    HashEntry<K,V> getFirst(int hash) {
        HashEntry<K,V>[] tab = table;
        return tab[hash & (tab.length - 1)];// 定位HashEntry所使用的hash算法
    }
    

    通过传入的第一次算出的hash值来拿到segment里面数组的下标,继而拿到数组里面链表头元素;
    如果拿出的value的值是null,则可能这个key,value健值对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value;

    ConcurrentHashMap的put操作
    put操作的前面也是确定Segment的过程,这里不再赘述,直接看关键的segment的put方法;

    V put(K key, int hash, V value, boolean onlyIfAbsent) {
        lock();
        try {
            int c = count;
            if (c++ > threshold) // ensure capacity
                rehash(); //第五行
            HashEntry<K,V>[] tab = table;
            int index = hash & (tab.length - 1);
            HashEntry<K,V> first = tab[index];  //第八行
            HashEntry<K,V> e = first; //第九行
            while (e != null && (e.hash != hash || !key.equals(e.key)))
                e = e.next; //第十一行
      
            V oldValue;
            if (e != null) {
                oldValue = e.value;
                if (!onlyIfAbsent)
                    e.value = value;
            } else {
                oldValue = null;
                ++modCount;
                tab[index] = new HashEntry<K,V>(key, hash, first, value); //第二十一行
                count = c; // write-volatile
            }
            return oldValue;
        } finally {
            unlock();
        }
    }
    

    首先对Segment的put操作是加锁完成的,然后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash;
      第8和第9行的操作就是getFirst的过程,确定链表头部的位置;
      第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素,如果找到,就直接更新更新key的value,如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值;

    ConcurrentHashMap的remove操作
    Remove操作的前面一部分和前面的get和put操作一样,都是定位Segment的过程,然后再调用Segment的remove方法

    V remove(Object key, int hash, Object value) {
        lock();
        try {
            int c = count - 1;
            HashEntry<K,V>[] tab = table;
            int index = hash & (tab.length - 1);
            HashEntry<K,V> first = tab[index];
            HashEntry<K,V> e = first;
            while (e != null && (e.hash != hash || !key.equals(e.key)))
                e = e.next;
      
            V oldValue = null;
            if (e != null) {
                V v = e.value;
                if (value == null || value.equals(v)) {
                    oldValue = v;
                    // All entries following removed node can stay
                    // in list, but all preceding ones need to be
                    // cloned.
                    ++modCount;
                    HashEntry<K,V> newFirst = e.next;
                    for (HashEntry<K,V> p = first; p != e; p = p.next)
                        newFirst = new HashEntry<K,V>(p.key, p.hash,
                                                      newFirst, p.value);
                    tab[index] = newFirst;
                    count = c; // write-volatile
                }
            }
            return oldValue;
        } finally {
            unlock();
        }
    }
    

    首先remove操作也是确定需要删除的元素的位置,不过这里删除元素的方法不是简单地把待删除元素的前面的一个元素的next指向后面一个就完事了,因为HashEntry中的next以及key与hash是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去;
    如下图:


    ConcurrentHashMap的size操作
    上面提到的get、put与remove的操作都是在单个Segment中执行的;而size()操作是在多个Segment中进行,

    要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效;

    ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁;
    Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了;

    相关文章

      网友评论

        本文标题:Java之Hashtalbe与Currenthashmap实现原

        本文链接:https://www.haomeiwen.com/subject/kedqnttx.html